Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
34215db1
Unverified
提交
34215db1
authored
8月 19, 2025
作者:
Will Chen
提交者:
GitHub
8月 19, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Help chat (#1007)
上级
0cdd13dc
显示空白字符变更
内嵌
并排
正在显示
8 个修改的文件
包含
655 行增加
和
0 行删除
+655
-0
HelpBotDialog.tsx
src/components/HelpBotDialog.tsx
+244
-0
HelpDialog.tsx
src/components/HelpDialog.tsx
+30
-0
LoadingBlock.tsx
src/components/LoadingBlock.tsx
+136
-0
help_bot_handlers.ts
src/ipc/handlers/help_bot_handlers.ts
+134
-0
ipc_client.ts
src/ipc/ipc_client.ts
+75
-0
ipc_host.ts
src/ipc/ipc_host.ts
+2
-0
ipc_types.ts
src/ipc/ipc_types.ts
+27
-0
preload.ts
src/preload.ts
+7
-0
没有找到文件。
src/components/HelpBotDialog.tsx
0 → 100644
浏览文件 @
34215db1
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
;
import
{
Dialog
,
DialogContent
,
DialogHeader
,
DialogTitle
,
}
from
"@/components/ui/dialog"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
v4
as
uuidv4
}
from
"uuid"
;
import
{
LoadingBlock
,
VanillaMarkdownParser
}
from
"@/components/LoadingBlock"
;
interface
HelpBotDialogProps
{
isOpen
:
boolean
;
onClose
:
()
=>
void
;
}
interface
Message
{
role
:
"user"
|
"assistant"
;
content
:
string
;
reasoning
?:
string
;
}
export
function
HelpBotDialog
({
isOpen
,
onClose
}:
HelpBotDialogProps
)
{
const
[
input
,
setInput
]
=
useState
(
""
);
const
[
messages
,
setMessages
]
=
useState
<
Message
[]
>
([]);
const
[
streaming
,
setStreaming
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
<
string
|
null
>
(
null
);
const
assistantBufferRef
=
useRef
(
""
);
const
reasoningBufferRef
=
useRef
(
""
);
const
flushTimerRef
=
useRef
<
number
|
null
>
(
null
);
const
FLUSH_INTERVAL_MS
=
100
;
const
sessionId
=
useMemo
(()
=>
uuidv4
(),
[
isOpen
]);
useEffect
(()
=>
{
if
(
!
isOpen
)
{
// Clean up when dialog closes
setMessages
([]);
setInput
(
""
);
setError
(
null
);
assistantBufferRef
.
current
=
""
;
reasoningBufferRef
.
current
=
""
;
// Clear the flush timer
if
(
flushTimerRef
.
current
)
{
window
.
clearInterval
(
flushTimerRef
.
current
);
flushTimerRef
.
current
=
null
;
}
}
},
[
isOpen
]);
// Cleanup on component unmount
useEffect
(()
=>
{
return
()
=>
{
// Clear the flush timer on unmount
if
(
flushTimerRef
.
current
)
{
window
.
clearInterval
(
flushTimerRef
.
current
);
flushTimerRef
.
current
=
null
;
}
};
},
[]);
const
handleSend
=
async
()
=>
{
const
trimmed
=
input
.
trim
();
if
(
!
trimmed
||
streaming
)
return
;
setError
(
null
);
// Clear any previous errors
setMessages
((
prev
)
=>
[
...
prev
,
{
role
:
"user"
,
content
:
trimmed
},
{
role
:
"assistant"
,
content
:
""
,
reasoning
:
""
},
]);
assistantBufferRef
.
current
=
""
;
reasoningBufferRef
.
current
=
""
;
setInput
(
""
);
setStreaming
(
true
);
IpcClient
.
getInstance
().
startHelpChat
(
sessionId
,
trimmed
,
{
onChunk
:
(
delta
)
=>
{
// Buffer assistant content; UI will flush on interval for smoothness
assistantBufferRef
.
current
+=
delta
;
},
onEnd
:
()
=>
{
// Final flush then stop streaming
setMessages
((
prev
)
=>
{
const
next
=
[...
prev
];
const
lastIdx
=
next
.
length
-
1
;
if
(
lastIdx
>=
0
&&
next
[
lastIdx
].
role
===
"assistant"
)
{
next
[
lastIdx
]
=
{
...
next
[
lastIdx
],
content
:
assistantBufferRef
.
current
,
reasoning
:
reasoningBufferRef
.
current
,
};
}
return
next
;
});
setStreaming
(
false
);
if
(
flushTimerRef
.
current
)
{
window
.
clearInterval
(
flushTimerRef
.
current
);
flushTimerRef
.
current
=
null
;
}
},
onError
:
(
errorMessage
:
string
)
=>
{
setError
(
errorMessage
);
setStreaming
(
false
);
// Clear the flush timer
if
(
flushTimerRef
.
current
)
{
window
.
clearInterval
(
flushTimerRef
.
current
);
flushTimerRef
.
current
=
null
;
}
// Clear the buffers
assistantBufferRef
.
current
=
""
;
reasoningBufferRef
.
current
=
""
;
// Remove the empty assistant message that was added optimistically
setMessages
((
prev
)
=>
{
const
next
=
[...
prev
];
if
(
next
.
length
>
0
&&
next
[
next
.
length
-
1
].
role
===
"assistant"
&&
!
next
[
next
.
length
-
1
].
content
)
{
next
.
pop
();
}
return
next
;
});
},
});
// Start smooth flush interval
if
(
flushTimerRef
.
current
)
{
window
.
clearInterval
(
flushTimerRef
.
current
);
}
flushTimerRef
.
current
=
window
.
setInterval
(()
=>
{
setMessages
((
prev
)
=>
{
const
next
=
[...
prev
];
const
lastIdx
=
next
.
length
-
1
;
if
(
lastIdx
>=
0
&&
next
[
lastIdx
].
role
===
"assistant"
)
{
const
current
=
next
[
lastIdx
];
// Only update if there's any new data to apply
if
(
current
.
content
!==
assistantBufferRef
.
current
||
current
.
reasoning
!==
reasoningBufferRef
.
current
)
{
next
[
lastIdx
]
=
{
...
current
,
content
:
assistantBufferRef
.
current
,
reasoning
:
reasoningBufferRef
.
current
,
};
}
}
return
next
;
});
},
FLUSH_INTERVAL_MS
);
};
return
(
<
Dialog
open=
{
isOpen
}
onOpenChange=
{
onClose
}
>
<
DialogContent
className=
"max-w-2xl"
>
<
DialogHeader
>
<
DialogTitle
>
Dyad Help Bot
</
DialogTitle
>
</
DialogHeader
>
<
div
className=
"flex flex-col gap-3 h-[480px]"
>
{
error
&&
(
<
div
className=
"bg-destructive/10 border border-destructive/20 rounded-md p-3"
>
<
div
className=
"flex items-start gap-2"
>
<
div
className=
"text-destructive text-sm font-medium"
>
Error:
</
div
>
<
div
className=
"text-destructive text-sm flex-1"
>
{
error
}
</
div
>
<
button
onClick=
{
()
=>
setError
(
null
)
}
className=
"text-destructive hover:text-destructive/80 text-xs"
>
✕
</
button
>
</
div
>
</
div
>
)
}
<
div
className=
"flex-1 overflow-auto rounded-md border p-3 bg-(--background-lightest)"
>
{
messages
.
length
===
0
?
(
<
div
className=
"space-y-3"
>
<
div
className=
"text-sm text-muted-foreground"
>
Ask a question about using Dyad.
</
div
>
<
div
className=
"text-xs text-muted-foreground/70 bg-muted/50 rounded-md p-3"
>
This conversation may be logged and used to improve the
product. Please do not put any sensitive information in here.
</
div
>
</
div
>
)
:
(
<
div
className=
"space-y-3"
>
{
messages
.
map
((
m
,
i
)
=>
(
<
div
key=
{
i
}
>
{
m
.
role
===
"user"
?
(
<
div
className=
"text-right"
>
<
div
className=
"inline-block rounded-lg px-3 py-2 bg-primary text-primary-foreground"
>
{
m
.
content
}
</
div
>
</
div
>
)
:
(
<
div
className=
"text-left"
>
{
streaming
&&
i
===
messages
.
length
-
1
&&
(
<
LoadingBlock
isStreaming=
{
streaming
&&
i
===
messages
.
length
-
1
}
/>
)
}
{
m
.
content
&&
(
<
div
className=
"inline-block rounded-lg px-3 py-2 bg-muted prose dark:prose-invert prose-headings:mb-2 prose-p:my-1 prose-pre:my-0 max-w-none"
>
<
VanillaMarkdownParser
content=
{
m
.
content
}
/>
</
div
>
)
}
</
div
>
)
}
</
div
>
))
}
</
div
>
)
}
</
div
>
<
div
className=
"flex gap-2"
>
<
input
className=
"flex-1 h-10 rounded-md border bg-background px-3 text-sm"
value=
{
input
}
onChange=
{
(
e
)
=>
setInput
(
e
.
target
.
value
)
}
placeholder=
"Type your question..."
onKeyDown=
{
(
e
)
=>
{
if
(
e
.
key
===
"Enter"
&&
!
e
.
shiftKey
)
{
e
.
preventDefault
();
handleSend
();
}
}
}
/>
<
Button
onClick=
{
handleSend
}
disabled=
{
streaming
||
!
input
.
trim
()
}
>
{
streaming
?
"Sending..."
:
"Send"
}
</
Button
>
</
div
>
</
div
>
</
DialogContent
>
</
Dialog
>
);
}
src/components/HelpDialog.tsx
浏览文件 @
34215db1
...
@@ -15,6 +15,7 @@ import {
...
@@ -15,6 +15,7 @@ import {
CheckIcon
,
CheckIcon
,
XIcon
,
XIcon
,
FileIcon
,
FileIcon
,
SparklesIcon
,
}
from
"lucide-react"
;
}
from
"lucide-react"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
useState
,
useEffect
}
from
"react"
;
import
{
useState
,
useEffect
}
from
"react"
;
...
@@ -22,6 +23,8 @@ import { useAtomValue } from "jotai";
...
@@ -22,6 +23,8 @@ import { useAtomValue } from "jotai";
import
{
selectedChatIdAtom
}
from
"@/atoms/chatAtoms"
;
import
{
selectedChatIdAtom
}
from
"@/atoms/chatAtoms"
;
import
{
ChatLogsData
}
from
"@/ipc/ipc_types"
;
import
{
ChatLogsData
}
from
"@/ipc/ipc_types"
;
import
{
showError
}
from
"@/lib/toast"
;
import
{
showError
}
from
"@/lib/toast"
;
import
{
HelpBotDialog
}
from
"./HelpBotDialog"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
interface
HelpDialogProps
{
interface
HelpDialogProps
{
isOpen
:
boolean
;
isOpen
:
boolean
;
...
@@ -35,7 +38,11 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
...
@@ -35,7 +38,11 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
const
[
chatLogsData
,
setChatLogsData
]
=
useState
<
ChatLogsData
|
null
>
(
null
);
const
[
chatLogsData
,
setChatLogsData
]
=
useState
<
ChatLogsData
|
null
>
(
null
);
const
[
uploadComplete
,
setUploadComplete
]
=
useState
(
false
);
const
[
uploadComplete
,
setUploadComplete
]
=
useState
(
false
);
const
[
sessionId
,
setSessionId
]
=
useState
(
""
);
const
[
sessionId
,
setSessionId
]
=
useState
(
""
);
const
[
isHelpBotOpen
,
setIsHelpBotOpen
]
=
useState
(
false
);
const
selectedChatId
=
useAtomValue
(
selectedChatIdAtom
);
const
selectedChatId
=
useAtomValue
(
selectedChatIdAtom
);
const
{
settings
}
=
useSettings
();
const
isDyadProUser
=
settings
?.
providerSettings
?.[
"auto"
]?.
apiKey
?.
value
;
// Function to reset all dialog state
// Function to reset all dialog state
const
resetDialogState
=
()
=>
{
const
resetDialogState
=
()
=>
{
...
@@ -373,6 +380,24 @@ Session ID: ${sessionId}
...
@@ -373,6 +380,24 @@ Session ID: ${sessionId}
If you need help or want to report an issue, here are some options:
If you need help or want to report an issue, here are some options:
</
DialogDescription
>
</
DialogDescription
>
<
div
className=
"flex flex-col space-y-4 w-full"
>
<
div
className=
"flex flex-col space-y-4 w-full"
>
{
isDyadProUser
?
(
<
div
className=
"flex flex-col space-y-2"
>
<
Button
variant=
"default"
onClick=
{
()
=>
{
setIsHelpBotOpen
(
true
);
}
}
className=
"w-full py-6 border-primary/50 shadow-sm shadow-primary/10 transition-all hover:shadow-md hover:shadow-primary/15"
>
<
SparklesIcon
className=
"mr-2 h-5 w-5"
/>
Chat with Dyad help
bot (Pro)
</
Button
>
<
p
className=
"text-sm text-muted-foreground px-2"
>
Opens an in-app help chat assistant that searches through Dyad's
docs.
</
p
>
</
div
>
)
:
(
<
div
className=
"flex flex-col space-y-2"
>
<
div
className=
"flex flex-col space-y-2"
>
<
Button
<
Button
variant=
"outline"
variant=
"outline"
...
@@ -389,6 +414,7 @@ Session ID: ${sessionId}
...
@@ -389,6 +414,7 @@ Session ID: ${sessionId}
Get help with common questions and issues.
Get help with common questions and issues.
</
p
>
</
p
>
</
div
>
</
div
>
)
}
<
div
className=
"flex flex-col space-y-2"
>
<
div
className=
"flex flex-col space-y-2"
>
<
Button
<
Button
...
@@ -422,6 +448,10 @@ Session ID: ${sessionId}
...
@@ -422,6 +448,10 @@ Session ID: ${sessionId}
</
div
>
</
div
>
</
div
>
</
div
>
</
DialogContent
>
</
DialogContent
>
<
HelpBotDialog
isOpen=
{
isHelpBotOpen
}
onClose=
{
()
=>
setIsHelpBotOpen
(
false
)
}
/>
</
Dialog
>
</
Dialog
>
);
);
}
}
src/components/LoadingBlock.tsx
0 → 100644
浏览文件 @
34215db1
import
{
useEffect
,
useState
}
from
"react"
;
import
ReactMarkdown
from
"react-markdown"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
const
customLink
=
({
node
:
_node
,
...
props
}:
{
node
?:
any
;
[
key
:
string
]:
any
;
})
=>
(
<
a
{
...
props
}
onClick=
{
(
e
)
=>
{
const
url
=
props
.
href
;
if
(
url
)
{
e
.
preventDefault
();
IpcClient
.
getInstance
().
openExternalUrl
(
url
);
}
}
}
/>
);
export
const
VanillaMarkdownParser
=
({
content
}:
{
content
:
string
})
=>
{
return
(
<
ReactMarkdown
components=
{
{
a
:
customLink
,
}
}
>
{
content
}
</
ReactMarkdown
>
);
};
// Chat loader with human-like typing/deleting of rotating messages
function
ChatLoader
()
{
const
[
currentTextIndex
,
setCurrentTextIndex
]
=
useState
(
0
);
const
[
displayText
,
setDisplayText
]
=
useState
(
""
);
const
[
isDeleting
,
setIsDeleting
]
=
useState
(
false
);
const
[
typingSpeed
,
setTypingSpeed
]
=
useState
(
100
);
const
loadingTexts
=
[
"Preparing your conversation... 🗨️"
,
"Gathering thoughts... 💭"
,
"Crafting the perfect response... 🎨"
,
"Almost there... 🚀"
,
"Just a moment... ⏳"
,
"Warming up the neural networks... 🧠"
,
"Connecting the dots... 🔗"
,
"Brewing some digital magic... ✨"
,
"Assembling words with care... 🔤"
,
"Fine-tuning the response... 🎯"
,
"Diving into deep thought... 🤿"
,
"Weaving ideas together... 🕸️"
,
"Sparking up the conversation... ⚡"
,
"Polishing the perfect reply... 💎"
,
];
useEffect
(()
=>
{
const
currentText
=
loadingTexts
[
currentTextIndex
];
const
timer
=
window
.
setTimeout
(()
=>
{
if
(
!
isDeleting
)
{
if
(
displayText
.
length
<
currentText
.
length
)
{
setDisplayText
(
currentText
.
substring
(
0
,
displayText
.
length
+
1
));
const
randomSpeed
=
Math
.
random
()
*
50
+
30
;
const
isLongPause
=
Math
.
random
()
>
0.85
;
setTypingSpeed
(
isLongPause
?
300
:
randomSpeed
);
}
else
{
setTypingSpeed
(
1500
);
setIsDeleting
(
true
);
}
}
else
{
if
(
displayText
.
length
>
0
)
{
setDisplayText
(
currentText
.
substring
(
0
,
displayText
.
length
-
1
));
setTypingSpeed
(
30
);
}
else
{
setIsDeleting
(
false
);
setCurrentTextIndex
((
prev
)
=>
(
prev
+
1
)
%
loadingTexts
.
length
);
setTypingSpeed
(
500
);
}
}
},
typingSpeed
);
return
()
=>
window
.
clearTimeout
(
timer
);
},
[
displayText
,
isDeleting
,
currentTextIndex
,
typingSpeed
]);
const
renderFadingText
=
()
=>
{
return
displayText
.
split
(
""
).
map
((
char
,
index
)
=>
{
const
opacity
=
Math
.
min
(
0.8
+
(
index
/
(
displayText
.
length
||
1
))
*
0.2
,
1
,
);
const
isEmoji
=
/
\p
{Emoji}/
u
.
test
(
char
);
return
(
<
span
key=
{
index
}
style=
{
{
opacity
}
}
className=
{
isEmoji
?
"inline-block animate-emoji-bounce"
:
""
}
>
{
char
}
</
span
>
);
});
};
return
(
<
div
className=
"flex flex-col items-center justify-center p-4"
>
<
style
>
{
`
@keyframes blink { from, to { opacity: 0 } 50% { opacity: 1 } }
@keyframes emoji-bounce { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(-2px) } }
@keyframes text-pulse { 0%, 100% { opacity: .85 } 50% { opacity: 1 } }
.animate-blink { animation: blink 1s steps(2, start) infinite; }
.animate-emoji-bounce { animation: emoji-bounce 1.2s ease-in-out infinite; }
.animate-text-pulse { animation: text-pulse 1.8s ease-in-out infinite; }
`
}
</
style
>
<
div
className=
"text-center animate-text-pulse"
>
<
div
className=
"inline-block"
>
<
p
className=
"text-sm text-gray-500 dark:text-gray-400 font-medium"
>
{
renderFadingText
()
}
<
span
className=
"ml-1 inline-block w-2 h-4 bg-gray-500 dark:bg-gray-400 animate-blink"
/>
</
p
>
</
div
>
</
div
>
</
div
>
);
}
interface
LoadingBlockProps
{
isStreaming
?:
boolean
;
}
// Instead of showing raw thinking content, render the chat loader while streaming.
export
function
LoadingBlock
({
isStreaming
=
false
}:
LoadingBlockProps
)
{
if
(
!
isStreaming
)
return
null
;
return
<
ChatLoader
/>;
}
src/ipc/handlers/help_bot_handlers.ts
0 → 100644
浏览文件 @
34215db1
import
{
ipcMain
}
from
"electron"
;
import
{
streamText
}
from
"ai"
;
import
{
readSettings
}
from
"../../main/settings"
;
import
log
from
"electron-log"
;
import
{
safeSend
}
from
"../utils/safe_sender"
;
import
{
createOpenAI
,
openai
,
OpenAIResponsesProviderOptions
,
}
from
"@ai-sdk/openai"
;
import
{
StartHelpChatParams
}
from
"../ipc_types"
;
const
logger
=
log
.
scope
(
"help-bot"
);
// In-memory session store for help bot conversations
type
HelpMessage
=
{
role
:
"user"
|
"assistant"
;
content
:
string
};
const
helpSessions
=
new
Map
<
string
,
HelpMessage
[]
>
();
const
activeHelpStreams
=
new
Map
<
string
,
AbortController
>
();
export
function
registerHelpBotHandlers
()
{
ipcMain
.
handle
(
"help:chat:start"
,
async
(
event
,
params
:
StartHelpChatParams
)
=>
{
const
{
sessionId
,
message
}
=
params
;
try
{
if
(
!
sessionId
||
!
message
?.
trim
())
{
throw
new
Error
(
"Missing sessionId or message"
);
}
// Clear any existing active streams (only one session at a time)
for
(
const
[
existingSessionId
,
controller
]
of
activeHelpStreams
)
{
controller
.
abort
();
activeHelpStreams
.
delete
(
existingSessionId
);
helpSessions
.
delete
(
existingSessionId
);
}
// Append user message to session history
const
history
=
helpSessions
.
get
(
sessionId
)
??
[];
const
updatedHistory
:
HelpMessage
[]
=
[
...
history
,
{
role
:
"user"
,
content
:
message
},
];
const
abortController
=
new
AbortController
();
activeHelpStreams
.
set
(
sessionId
,
abortController
);
const
settings
=
await
readSettings
();
const
apiKey
=
settings
.
providerSettings
?.[
"auto"
]?.
apiKey
?.
value
;
const
provider
=
createOpenAI
({
baseURL
:
"https://helpchat.dyad.sh/v1"
,
apiKey
,
});
let
assistantContent
=
""
;
const
stream
=
streamText
({
model
:
provider
.
responses
(
"gpt-5-nano"
),
providerOptions
:
{
openai
:
{
reasoningSummary
:
"auto"
,
}
satisfies
OpenAIResponsesProviderOptions
,
},
tools
:
{
web_search_preview
:
openai
.
tools
.
webSearchPreview
({
searchContextSize
:
"high"
,
}),
},
messages
:
updatedHistory
as
any
,
maxRetries
:
1
,
onError
:
(
error
)
=>
{
let
errorMessage
=
(
error
as
any
)?.
error
?.
message
;
logger
.
error
(
"help bot stream error"
,
errorMessage
);
safeSend
(
event
.
sender
,
"help:chat:response:error"
,
{
sessionId
,
error
:
String
(
errorMessage
),
});
},
});
(
async
()
=>
{
try
{
for
await
(
const
part
of
stream
.
fullStream
)
{
if
(
abortController
.
signal
.
aborted
)
break
;
if
(
part
.
type
===
"text-delta"
)
{
assistantContent
+=
part
.
text
;
safeSend
(
event
.
sender
,
"help:chat:response:chunk"
,
{
sessionId
,
delta
:
part
.
text
,
type
:
"text"
,
});
}
}
// Finalize session history
const
finalHistory
:
HelpMessage
[]
=
[
...
updatedHistory
,
{
role
:
"assistant"
,
content
:
assistantContent
},
];
helpSessions
.
set
(
sessionId
,
finalHistory
);
safeSend
(
event
.
sender
,
"help:chat:response:end"
,
{
sessionId
});
}
catch
(
err
)
{
if
((
err
as
any
)?.
name
===
"AbortError"
)
{
logger
.
log
(
"help bot stream aborted"
,
sessionId
);
return
;
}
logger
.
error
(
"help bot stream loop error"
,
err
);
safeSend
(
event
.
sender
,
"help:chat:response:error"
,
{
sessionId
,
error
:
String
(
err
instanceof
Error
?
err
.
message
:
err
),
});
}
finally
{
activeHelpStreams
.
delete
(
sessionId
);
}
})();
return
{
ok
:
true
}
as
const
;
}
catch
(
err
)
{
logger
.
error
(
"help:chat:start error"
,
err
);
throw
err
instanceof
Error
?
err
:
new
Error
(
String
(
err
));
}
},
);
ipcMain
.
handle
(
"help:chat:cancel"
,
async
(
_event
,
sessionId
:
string
)
=>
{
const
controller
=
activeHelpStreams
.
get
(
sessionId
);
if
(
controller
)
{
controller
.
abort
();
activeHelpStreams
.
delete
(
sessionId
);
}
return
{
ok
:
true
}
as
const
;
});
}
src/ipc/ipc_client.ts
浏览文件 @
34215db1
...
@@ -104,10 +104,19 @@ export class IpcClient {
...
@@ -104,10 +104,19 @@ export class IpcClient {
private
ipcRenderer
:
IpcRenderer
;
private
ipcRenderer
:
IpcRenderer
;
private
chatStreams
:
Map
<
number
,
ChatStreamCallbacks
>
;
private
chatStreams
:
Map
<
number
,
ChatStreamCallbacks
>
;
private
appStreams
:
Map
<
number
,
AppStreamCallbacks
>
;
private
appStreams
:
Map
<
number
,
AppStreamCallbacks
>
;
private
helpStreams
:
Map
<
string
,
{
onChunk
:
(
delta
:
string
)
=>
void
;
onEnd
:
()
=>
void
;
onError
:
(
error
:
string
)
=>
void
;
}
>
;
private
constructor
()
{
private
constructor
()
{
this
.
ipcRenderer
=
(
window
as
any
).
electron
.
ipcRenderer
as
IpcRenderer
;
this
.
ipcRenderer
=
(
window
as
any
).
electron
.
ipcRenderer
as
IpcRenderer
;
this
.
chatStreams
=
new
Map
();
this
.
chatStreams
=
new
Map
();
this
.
appStreams
=
new
Map
();
this
.
appStreams
=
new
Map
();
this
.
helpStreams
=
new
Map
();
// Set up listeners for stream events
// Set up listeners for stream events
this
.
ipcRenderer
.
on
(
"chat:response:chunk"
,
(
data
)
=>
{
this
.
ipcRenderer
.
on
(
"chat:response:chunk"
,
(
data
)
=>
{
if
(
if
(
...
@@ -180,6 +189,48 @@ export class IpcClient {
...
@@ -180,6 +189,48 @@ export class IpcClient {
console
.
error
(
"[IPC] Invalid error data received:"
,
error
);
console
.
error
(
"[IPC] Invalid error data received:"
,
error
);
}
}
});
});
// Help bot events
this
.
ipcRenderer
.
on
(
"help:chat:response:chunk"
,
(
data
)
=>
{
if
(
data
&&
typeof
data
===
"object"
&&
"sessionId"
in
data
&&
"delta"
in
data
)
{
const
{
sessionId
,
delta
}
=
data
as
{
sessionId
:
string
;
delta
:
string
;
};
const
callbacks
=
this
.
helpStreams
.
get
(
sessionId
);
if
(
callbacks
)
callbacks
.
onChunk
(
delta
);
}
});
this
.
ipcRenderer
.
on
(
"help:chat:response:end"
,
(
data
)
=>
{
if
(
data
&&
typeof
data
===
"object"
&&
"sessionId"
in
data
)
{
const
{
sessionId
}
=
data
as
{
sessionId
:
string
};
const
callbacks
=
this
.
helpStreams
.
get
(
sessionId
);
if
(
callbacks
)
callbacks
.
onEnd
();
this
.
helpStreams
.
delete
(
sessionId
);
}
});
this
.
ipcRenderer
.
on
(
"help:chat:response:error"
,
(
data
)
=>
{
if
(
data
&&
typeof
data
===
"object"
&&
"sessionId"
in
data
&&
"error"
in
data
)
{
const
{
sessionId
,
error
}
=
data
as
{
sessionId
:
string
;
error
:
string
;
};
const
callbacks
=
this
.
helpStreams
.
get
(
sessionId
);
if
(
callbacks
)
callbacks
.
onError
(
error
);
this
.
helpStreams
.
delete
(
sessionId
);
}
});
}
}
public
static
getInstance
():
IpcClient
{
public
static
getInstance
():
IpcClient
{
...
@@ -1089,4 +1140,28 @@ export class IpcClient {
...
@@ -1089,4 +1140,28 @@ export class IpcClient {
public
async
deletePrompt
(
id
:
number
):
Promise
<
void
>
{
public
async
deletePrompt
(
id
:
number
):
Promise
<
void
>
{
await
this
.
ipcRenderer
.
invoke
(
"prompts:delete"
,
id
);
await
this
.
ipcRenderer
.
invoke
(
"prompts:delete"
,
id
);
}
}
// --- Help bot ---
public
startHelpChat
(
sessionId
:
string
,
message
:
string
,
options
:
{
onChunk
:
(
delta
:
string
)
=>
void
;
onEnd
:
()
=>
void
;
onError
:
(
error
:
string
)
=>
void
;
},
):
void
{
this
.
helpStreams
.
set
(
sessionId
,
options
);
this
.
ipcRenderer
.
invoke
(
"help:chat:start"
,
{
sessionId
,
message
})
.
catch
((
err
)
=>
{
this
.
helpStreams
.
delete
(
sessionId
);
showError
(
err
);
options
.
onError
(
String
(
err
));
});
}
public
cancelHelpChat
(
sessionId
:
string
):
void
{
this
.
ipcRenderer
.
invoke
(
"help:chat:cancel"
,
sessionId
).
catch
(()
=>
{});
}
}
}
src/ipc/ipc_host.ts
浏览文件 @
34215db1
...
@@ -29,6 +29,7 @@ import { registerAppEnvVarsHandlers } from "./handlers/app_env_vars_handlers";
...
@@ -29,6 +29,7 @@ import { registerAppEnvVarsHandlers } from "./handlers/app_env_vars_handlers";
import
{
registerTemplateHandlers
}
from
"./handlers/template_handlers"
;
import
{
registerTemplateHandlers
}
from
"./handlers/template_handlers"
;
import
{
registerPortalHandlers
}
from
"./handlers/portal_handlers"
;
import
{
registerPortalHandlers
}
from
"./handlers/portal_handlers"
;
import
{
registerPromptHandlers
}
from
"./handlers/prompt_handlers"
;
import
{
registerPromptHandlers
}
from
"./handlers/prompt_handlers"
;
import
{
registerHelpBotHandlers
}
from
"./handlers/help_bot_handlers"
;
export
function
registerIpcHandlers
()
{
export
function
registerIpcHandlers
()
{
// Register all IPC handlers by category
// Register all IPC handlers by category
...
@@ -63,4 +64,5 @@ export function registerIpcHandlers() {
...
@@ -63,4 +64,5 @@ export function registerIpcHandlers() {
registerTemplateHandlers
();
registerTemplateHandlers
();
registerPortalHandlers
();
registerPortalHandlers
();
registerPromptHandlers
();
registerPromptHandlers
();
registerHelpBotHandlers
();
}
}
src/ipc/ipc_types.ts
浏览文件 @
34215db1
...
@@ -420,3 +420,30 @@ export interface RevertVersionParams {
...
@@ -420,3 +420,30 @@ export interface RevertVersionParams {
export
type
RevertVersionResponse
=
export
type
RevertVersionResponse
=
|
{
successMessage
:
string
}
|
{
successMessage
:
string
}
|
{
warningMessage
:
string
};
|
{
warningMessage
:
string
};
// --- Help Bot Types ---
export
interface
StartHelpChatParams
{
sessionId
:
string
;
message
:
string
;
}
export
interface
HelpChatResponseChunk
{
sessionId
:
string
;
delta
:
string
;
type
:
"text"
;
}
export
interface
HelpChatResponseReasoning
{
sessionId
:
string
;
delta
:
string
;
type
:
"reasoning"
;
}
export
interface
HelpChatResponseEnd
{
sessionId
:
string
;
}
export
interface
HelpChatResponseError
{
sessionId
:
string
;
error
:
string
;
}
src/preload.ts
浏览文件 @
34215db1
...
@@ -106,6 +106,9 @@ const validInvokeChannels = [
...
@@ -106,6 +106,9 @@ const validInvokeChannels = [
"restart-dyad"
,
"restart-dyad"
,
"get-templates"
,
"get-templates"
,
"portal:migrate-create"
,
"portal:migrate-create"
,
// Help bot
"help:chat:start"
,
"help:chat:cancel"
,
// Prompts
// Prompts
"prompts:list"
,
"prompts:list"
,
"prompts:create"
,
"prompts:create"
,
...
@@ -128,6 +131,10 @@ const validReceiveChannels = [
...
@@ -128,6 +131,10 @@ const validReceiveChannels = [
"github:flow-success"
,
"github:flow-success"
,
"github:flow-error"
,
"github:flow-error"
,
"deep-link-received"
,
"deep-link-received"
,
// Help bot
"help:chat:response:chunk"
,
"help:chat:response:end"
,
"help:chat:response:error"
,
]
as
const
;
]
as
const
;
type
ValidInvokeChannel
=
(
typeof
validInvokeChannels
)[
number
];
type
ValidInvokeChannel
=
(
typeof
validInvokeChannels
)[
number
];
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论