Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
19d1e890
Unverified
提交
19d1e890
authored
4月 29, 2025
作者:
Will Chen
提交者:
GitHub
4月 29, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Create Upload Chat Session help feature (#48)
上级
74003b90
显示空白字符变更
内嵌
并排
正在显示
7 个修改的文件
包含
486 行增加
和
21 行删除
+486
-21
HelpDialog.tsx
src/components/HelpDialog.tsx
+307
-5
debug_handlers.ts
src/ipc/handlers/debug_handlers.ts
+79
-15
upload_handlers.ts
src/ipc/handlers/upload_handlers.ts
+59
-0
ipc_client.ts
src/ipc/ipc_client.ts
+30
-1
ipc_host.ts
src/ipc/ipc_host.ts
+2
-0
ipc_types.ts
src/ipc/ipc_types.ts
+7
-0
preload.ts
src/preload.ts
+2
-0
没有找到文件。
src/components/HelpDialog.tsx
浏览文件 @
19d1e890
...
...
@@ -7,9 +7,21 @@ import {
DialogFooter
,
}
from
"@/components/ui/dialog"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
BookOpenIcon
,
BugIcon
}
from
"lucide-react"
;
import
{
BookOpenIcon
,
BugIcon
,
UploadIcon
,
ChevronLeftIcon
,
CheckIcon
,
XIcon
,
FileIcon
,
}
from
"lucide-react"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
useState
}
from
"react"
;
import
{
useState
,
useEffect
}
from
"react"
;
import
{
useAtomValue
}
from
"jotai"
;
import
{
selectedChatIdAtom
}
from
"@/atoms/chatAtoms"
;
import
{
ChatLogsData
}
from
"@/ipc/ipc_types"
;
import
{
showError
}
from
"@/lib/toast"
;
interface
HelpDialogProps
{
isOpen
:
boolean
;
...
...
@@ -18,6 +30,34 @@ interface HelpDialogProps {
export
function
HelpDialog
({
isOpen
,
onClose
}:
HelpDialogProps
)
{
const
[
isLoading
,
setIsLoading
]
=
useState
(
false
);
const
[
isUploading
,
setIsUploading
]
=
useState
(
false
);
const
[
reviewMode
,
setReviewMode
]
=
useState
(
false
);
const
[
chatLogsData
,
setChatLogsData
]
=
useState
<
ChatLogsData
|
null
>
(
null
);
const
[
uploadComplete
,
setUploadComplete
]
=
useState
(
false
);
const
[
sessionId
,
setSessionId
]
=
useState
(
""
);
const
selectedChatId
=
useAtomValue
(
selectedChatIdAtom
);
// Function to reset all dialog state
const
resetDialogState
=
()
=>
{
setIsLoading
(
false
);
setIsUploading
(
false
);
setReviewMode
(
false
);
setChatLogsData
(
null
);
setUploadComplete
(
false
);
setSessionId
(
""
);
};
// Reset state when dialog closes or reopens
useEffect
(()
=>
{
if
(
!
isOpen
)
{
resetDialogState
();
}
},
[
isOpen
]);
// Wrap the original onClose to also reset state
const
handleClose
=
()
=>
{
onClose
();
};
const
handleReportBug
=
async
()
=>
{
setIsLoading
(
true
);
...
...
@@ -71,15 +111,261 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
}
};
const
handleUploadChatSession
=
async
()
=>
{
if
(
!
selectedChatId
)
{
alert
(
"Please select a chat first"
);
return
;
}
setIsUploading
(
true
);
try
{
// Get chat logs (includes debug info, chat data, and codebase)
const
chatLogs
=
await
IpcClient
.
getInstance
().
getChatLogs
(
selectedChatId
);
// Store data for review and switch to review mode
setChatLogsData
(
chatLogs
);
setReviewMode
(
true
);
}
catch
(
error
)
{
console
.
error
(
"Failed to upload chat session:"
,
error
);
alert
(
"Failed to upload chat session. Please try again or report manually."
);
}
finally
{
setIsUploading
(
false
);
}
};
const
handleSubmitChatLogs
=
async
()
=>
{
if
(
!
chatLogsData
)
return
;
setIsUploading
(
true
);
try
{
// Prepare data for upload
const
chatLogsJson
=
{
systemInfo
:
chatLogsData
.
debugInfo
,
chat
:
chatLogsData
.
chat
,
codebaseSnippet
:
chatLogsData
.
codebase
,
};
// Get signed URL
const
response
=
await
fetch
(
"https://upload-logs.dyad.sh/generate-upload-url"
,
{
method
:
"POST"
,
headers
:
{
"Content-Type"
:
"application/json"
,
},
body
:
JSON
.
stringify
({
extension
:
"json"
,
contentType
:
"application/json"
,
}),
}
);
if
(
!
response
.
ok
)
{
showError
(
`Failed to get upload URL:
${
response
.
statusText
}
`
);
throw
new
Error
(
`Failed to get upload URL:
${
response
.
statusText
}
`
);
}
const
{
uploadUrl
,
filename
}
=
await
response
.
json
();
// Upload to the signed URL using IPC
const
uploadResult
=
await
IpcClient
.
getInstance
().
uploadToSignedUrl
(
uploadUrl
,
"application/json"
,
chatLogsJson
);
if
(
!
uploadResult
.
success
)
{
throw
new
Error
(
`Failed to upload logs:
${
uploadResult
.
error
}
`
);
}
// Extract session ID (filename without extension)
const
sessionId
=
filename
.
replace
(
".json"
,
""
);
setSessionId
(
sessionId
);
setUploadComplete
(
true
);
setReviewMode
(
false
);
}
catch
(
error
)
{
console
.
error
(
"Failed to upload chat logs:"
,
error
);
alert
(
"Failed to upload chat logs. Please try again."
);
}
finally
{
setIsUploading
(
false
);
}
};
const
handleCancelReview
=
()
=>
{
setReviewMode
(
false
);
setChatLogsData
(
null
);
};
const
handleOpenGitHubIssue
=
()
=>
{
// Create a GitHub issue with the session ID
const
issueBody
=
`
## Support Request
Session ID:
${
sessionId
}
## Issue Description
<!-- Please describe the issue you're experiencing -->
## Expected Behavior
<!-- What did you expect to happen? -->
## Actual Behavior
<!-- What actually happened? -->
`
;
const
encodedBody
=
encodeURIComponent
(
issueBody
);
const
encodedTitle
=
encodeURIComponent
(
"[session report] <add title>"
);
const
githubIssueUrl
=
`https://github.com/dyad-sh/dyad/issues/new?title=
${
encodedTitle
}
&labels=support&body=
${
encodedBody
}
`
;
IpcClient
.
getInstance
().
openExternalUrl
(
githubIssueUrl
);
handleClose
();
};
if
(
uploadComplete
)
{
return
(
<
Dialog
open=
{
isOpen
}
onOpenChange=
{
onClose
}
>
<
Dialog
open=
{
isOpen
}
onOpenChange=
{
handleClose
}
>
<
DialogContent
>
<
DialogHeader
>
<
DialogTitle
>
Upload Complete
</
DialogTitle
>
</
DialogHeader
>
<
div
className=
"py-6 flex flex-col items-center space-y-4"
>
<
div
className=
"bg-green-50 dark:bg-green-900/20 p-6 rounded-full"
>
<
CheckIcon
className=
"h-8 w-8 text-green-600 dark:text-green-400"
/>
</
div
>
<
h3
className=
"text-lg font-medium"
>
Chat Logs Uploaded Successfully
</
h3
>
<
div
className=
"bg-slate-100 dark:bg-slate-800 p-3 rounded flex items-center space-x-2 font-mono text-sm"
>
<
FileIcon
className=
"h-4 w-4 cursor-pointer"
onClick=
{
async
()
=>
{
try
{
await
navigator
.
clipboard
.
writeText
(
sessionId
);
}
catch
(
err
)
{
console
.
error
(
"Failed to copy session ID:"
,
err
);
}
}
}
/>
<
span
>
{
sessionId
}
</
span
>
</
div
>
<
p
className=
"text-center text-sm"
>
Please open a GitHub issue so we can follow-up with you on this
issue.
</
p
>
</
div
>
<
DialogFooter
>
<
Button
onClick=
{
handleOpenGitHubIssue
}
className=
"w-full"
>
Open GitHub Issue
</
Button
>
</
DialogFooter
>
</
DialogContent
>
</
Dialog
>
);
}
if
(
reviewMode
&&
chatLogsData
)
{
return
(
<
Dialog
open=
{
isOpen
}
onOpenChange=
{
handleClose
}
>
<
DialogContent
className=
"max-w-4xl max-h-[80vh] overflow-hidden flex flex-col"
>
<
DialogHeader
>
<
DialogTitle
className=
"flex items-center"
>
<
Button
variant=
"ghost"
className=
"mr-2 p-0 h-8 w-8"
onClick=
{
handleCancelReview
}
>
<
ChevronLeftIcon
className=
"h-4 w-4"
/>
</
Button
>
OK to upload chat session?
</
DialogTitle
>
</
DialogHeader
>
<
DialogDescription
>
Please review the information that will be submitted. Your chat
messages, system information, and a snapshot of your codebase will
be included.
</
DialogDescription
>
<
div
className=
"space-y-4 overflow-y-auto flex-grow"
>
<
div
className=
"border rounded-md p-3"
>
<
h3
className=
"font-medium mb-2"
>
Chat Messages
</
h3
>
<
div
className=
"text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto"
>
{
chatLogsData
.
chat
.
messages
.
map
((
msg
,
index
)
=>
(
<
div
key=
{
msg
.
id
}
className=
"mb-2"
>
<
span
className=
"font-semibold"
>
{
msg
.
role
===
"user"
?
"You"
:
"Assistant"
}
:
{
" "
}
</
span
>
<
span
>
{
msg
.
content
}
</
span
>
</
div
>
))
}
</
div
>
</
div
>
<
div
className=
"border rounded-md p-3"
>
<
h3
className=
"font-medium mb-2"
>
Codebase Snapshot
</
h3
>
<
div
className=
"text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono"
>
{
chatLogsData
.
codebase
}
</
div
>
</
div
>
<
div
className=
"border rounded-md p-3"
>
<
h3
className=
"font-medium mb-2"
>
Logs
</
h3
>
<
div
className=
"text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono"
>
{
chatLogsData
.
debugInfo
.
logs
}
</
div
>
</
div
>
<
div
className=
"border rounded-md p-3"
>
<
h3
className=
"font-medium mb-2"
>
System Information
</
h3
>
<
div
className=
"text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-32 overflow-y-auto"
>
<
p
>
Dyad Version:
{
chatLogsData
.
debugInfo
.
dyadVersion
}
</
p
>
<
p
>
Platform:
{
chatLogsData
.
debugInfo
.
platform
}
</
p
>
<
p
>
Architecture:
{
chatLogsData
.
debugInfo
.
architecture
}
</
p
>
<
p
>
Node Version:
{
" "
}
{
chatLogsData
.
debugInfo
.
nodeVersion
||
"Not available"
}
</
p
>
</
div
>
</
div
>
</
div
>
<
div
className=
"flex justify-between mt-4 pt-2 sticky bottom-0 bg-background"
>
<
Button
variant=
"outline"
onClick=
{
handleCancelReview
}
className=
"flex items-center"
>
<
XIcon
className=
"mr-2 h-4 w-4"
/>
Cancel
</
Button
>
<
Button
onClick=
{
handleSubmitChatLogs
}
className=
"flex items-center"
disabled=
{
isUploading
}
>
{
isUploading
?
(
"Uploading..."
)
:
(
<>
<
CheckIcon
className=
"mr-2 h-4 w-4"
/>
Upload
</>
)
}
</
Button
>
</
div
>
</
DialogContent
>
</
Dialog
>
);
}
return
(
<
Dialog
open=
{
isOpen
}
onOpenChange=
{
handleClose
}
>
<
DialogContent
>
<
DialogHeader
>
<
DialogTitle
>
Need help with Dyad?
</
DialogTitle
>
</
DialogHeader
>
<
DialogDescription
className=
""
>
If you need assistance or want to report an issue, here are some
resources:
If you need help or want to report an issue, here are some options:
</
DialogDescription
>
<
div
className=
"flex flex-col space-y-4 w-full"
>
<
div
className=
"flex flex-col space-y-2"
>
...
...
@@ -98,6 +384,7 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
Get help with common questions and issues.
</
p
>
</
div
>
<
div
className=
"flex flex-col space-y-2"
>
<
Button
variant=
"outline"
...
...
@@ -113,6 +400,21 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
review it for any sensitive info before submitting.
</
p
>
</
div
>
<
div
className=
"flex flex-col space-y-2"
>
<
Button
variant=
"outline"
onClick=
{
handleUploadChatSession
}
disabled=
{
isUploading
||
!
selectedChatId
}
className=
"w-full py-6 bg-(--background-lightest)"
>
<
UploadIcon
className=
"mr-2 h-5 w-5"
/>
{
" "
}
{
isUploading
?
"Preparing Upload..."
:
"Upload Chat Session"
}
</
Button
>
<
p
className=
"text-sm text-muted-foreground px-2"
>
Share chat logs and code for troubleshooting. Data is used only to
resolve your issue and auto-deleted after a limited time.
</
p
>
</
div
>
</
div
>
</
DialogContent
>
</
Dialog
>
...
...
src/ipc/handlers/debug_handlers.ts
浏览文件 @
19d1e890
import
{
ipcMain
,
app
}
from
"electron"
;
import
{
platform
,
arch
}
from
"os"
;
import
{
SystemDebugInfo
}
from
"../ipc_types"
;
import
{
SystemDebugInfo
,
ChatLogsData
}
from
"../ipc_types"
;
import
{
readSettings
}
from
"../../main/settings"
;
import
{
execSync
}
from
"child_process"
;
import
log
from
"electron-log"
;
import
path
from
"path"
;
import
fs
from
"fs"
;
import
{
runShellCommand
}
from
"../utils/runShellCommand"
;
import
{
extractCodebase
}
from
"../../utils/codebase"
;
import
{
db
}
from
"../../db"
;
import
{
chats
,
apps
}
from
"../../db/schema"
;
import
{
eq
}
from
"drizzle-orm"
;
import
{
getDyadAppPath
}
from
"../../paths/paths"
;
export
function
registerDebugHandlers
()
{
ipcMain
.
handle
(
"get-system-debug-info"
,
async
():
Promise
<
SystemDebugInfo
>
=>
{
console
.
log
(
"IPC: get-system-debug-info called"
);
// Shared function to get system debug info
async
function
getSystemDebugInfo
():
Promise
<
SystemDebugInfo
>
{
console
.
log
(
"Getting system debug info"
);
// Get Node.js and pnpm versions
let
nodeVersion
:
string
|
null
=
null
;
...
...
@@ -41,17 +44,10 @@ export function registerDebugHandlers() {
}
// Get Dyad version from package.json
const
packageJsonPath
=
path
.
resolve
(
__dirname
,
".."
,
".."
,
"package.json"
);
const
packageJsonPath
=
path
.
resolve
(
__dirname
,
".."
,
".."
,
"package.json"
);
let
dyadVersion
=
"unknown"
;
try
{
const
packageJson
=
JSON
.
parse
(
fs
.
readFileSync
(
packageJsonPath
,
"utf8"
)
);
const
packageJson
=
JSON
.
parse
(
fs
.
readFileSync
(
packageJsonPath
,
"utf8"
));
dyadVersion
=
packageJson
.
version
;
}
catch
(
err
)
{
console
.
error
(
"Failed to read package.json:"
,
err
);
...
...
@@ -87,6 +83,74 @@ export function registerDebugHandlers() {
architecture
:
arch
(),
logs
,
};
}
export
function
registerDebugHandlers
()
{
ipcMain
.
handle
(
"get-system-debug-info"
,
async
():
Promise
<
SystemDebugInfo
>
=>
{
console
.
log
(
"IPC: get-system-debug-info called"
);
return
getSystemDebugInfo
();
}
);
ipcMain
.
handle
(
"get-chat-logs"
,
async
(
_
,
chatId
:
number
):
Promise
<
ChatLogsData
>
=>
{
console
.
log
(
`IPC: get-chat-logs called for chat
${
chatId
}
`
);
try
{
// Get system debug info using the shared function
const
debugInfo
=
await
getSystemDebugInfo
();
// Get chat data from database
const
chatRecord
=
await
db
.
query
.
chats
.
findFirst
({
where
:
eq
(
chats
.
id
,
chatId
),
with
:
{
messages
:
{
orderBy
:
(
messages
,
{
asc
})
=>
[
asc
(
messages
.
createdAt
)],
},
},
});
if
(
!
chatRecord
)
{
throw
new
Error
(
`Chat with ID
${
chatId
}
not found`
);
}
// Format the chat to match the Chat interface
const
chat
=
{
id
:
chatRecord
.
id
,
title
:
chatRecord
.
title
||
"Untitled Chat"
,
messages
:
chatRecord
.
messages
.
map
((
msg
)
=>
({
id
:
msg
.
id
,
role
:
msg
.
role
,
content
:
msg
.
content
,
approvalState
:
msg
.
approvalState
,
})),
};
// Get app data from database
const
app
=
await
db
.
query
.
apps
.
findFirst
({
where
:
eq
(
apps
.
id
,
chatRecord
.
appId
),
});
if
(
!
app
)
{
throw
new
Error
(
`App with ID
${
chatRecord
.
appId
}
not found`
);
}
// Extract codebase
const
appPath
=
getDyadAppPath
(
app
.
path
);
const
codebase
=
await
extractCodebase
(
appPath
);
return
{
debugInfo
,
chat
,
codebase
,
};
}
catch
(
error
)
{
console
.
error
(
`Error in get-chat-logs:`
,
error
);
throw
error
;
}
}
);
...
...
src/ipc/handlers/upload_handlers.ts
0 → 100644
浏览文件 @
19d1e890
import
{
ipcMain
}
from
"electron"
;
import
log
from
"electron-log"
;
import
fetch
from
"node-fetch"
;
const
logger
=
log
.
scope
(
"upload_handlers"
);
interface
UploadToSignedUrlParams
{
url
:
string
;
contentType
:
string
;
data
:
any
;
}
export
function
registerUploadHandlers
()
{
ipcMain
.
handle
(
"upload-to-signed-url"
,
async
(
_
,
params
:
UploadToSignedUrlParams
)
=>
{
const
{
url
,
contentType
,
data
}
=
params
;
logger
.
debug
(
"IPC: upload-to-signed-url called"
);
try
{
// Validate the signed URL
if
(
!
url
||
typeof
url
!==
"string"
||
!
url
.
startsWith
(
"https://"
))
{
throw
new
Error
(
"Invalid signed URL provided"
);
}
// Validate content type
if
(
!
contentType
||
typeof
contentType
!==
"string"
)
{
throw
new
Error
(
"Invalid content type provided"
);
}
// Perform the upload to the signed URL
const
response
=
await
fetch
(
url
,
{
method
:
"PUT"
,
headers
:
{
"Content-Type"
:
contentType
,
},
body
:
JSON
.
stringify
(
data
),
});
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Upload failed with status
${
response
.
status
}
:
${
response
.
statusText
}
`
);
}
logger
.
debug
(
"Successfully uploaded data to signed URL"
);
return
{
success
:
true
};
}
catch
(
error
)
{
logger
.
error
(
"Failed to upload to signed URL:"
,
error
);
return
{
success
:
false
,
error
:
error
instanceof
Error
?
error
.
message
:
String
(
error
),
};
}
}
);
logger
.
debug
(
"Registered upload IPC handlers"
);
}
src/ipc/ipc_client.ts
浏览文件 @
19d1e890
...
...
@@ -21,6 +21,7 @@ import type {
LocalModelListResponse
,
TokenCountParams
,
TokenCountResult
,
ChatLogsData
,
}
from
"./ipc_types"
;
import
type
{
CodeProposal
,
ProposalResult
}
from
"@/lib/schemas"
;
import
{
showError
}
from
"@/lib/toast"
;
...
...
@@ -749,7 +750,35 @@ export class IpcClient {
public
async
getSystemDebugInfo
():
Promise
<
SystemDebugInfo
>
{
try
{
const
data
=
await
this
.
ipcRenderer
.
invoke
(
"get-system-debug-info"
);
return
data
;
return
data
as
SystemDebugInfo
;
}
catch
(
error
)
{
showError
(
error
);
throw
error
;
}
}
public
async
getChatLogs
(
chatId
:
number
):
Promise
<
ChatLogsData
>
{
try
{
const
data
=
await
this
.
ipcRenderer
.
invoke
(
"get-chat-logs"
,
chatId
);
return
data
as
ChatLogsData
;
}
catch
(
error
)
{
showError
(
error
);
throw
error
;
}
}
public
async
uploadToSignedUrl
(
url
:
string
,
contentType
:
string
,
data
:
any
):
Promise
<
{
success
:
boolean
;
error
?:
string
}
>
{
try
{
const
result
=
await
this
.
ipcRenderer
.
invoke
(
"upload-to-signed-url"
,
{
url
,
contentType
,
data
,
});
return
result
as
{
success
:
boolean
;
error
?:
string
};
}
catch
(
error
)
{
showError
(
error
);
throw
error
;
...
...
src/ipc/ipc_host.ts
浏览文件 @
19d1e890
...
...
@@ -12,6 +12,7 @@ import { registerSupabaseHandlers } from "./handlers/supabase_handlers";
import
{
registerLocalModelHandlers
}
from
"./handlers/local_model_handlers"
;
import
{
registerTokenCountHandlers
}
from
"./handlers/token_count_handlers"
;
import
{
registerWindowHandlers
}
from
"./handlers/window_handlers"
;
import
{
registerUploadHandlers
}
from
"./handlers/upload_handlers"
;
export
function
registerIpcHandlers
()
{
// Register all IPC handlers by category
...
...
@@ -29,4 +30,5 @@ export function registerIpcHandlers() {
registerLocalModelHandlers
();
registerTokenCountHandlers
();
registerWindowHandlers
();
registerUploadHandlers
();
}
src/ipc/ipc_types.ts
浏览文件 @
19d1e890
...
...
@@ -106,6 +106,7 @@ export interface TokenCountParams {
chatId
:
number
;
input
:
string
;
}
export
interface
TokenCountResult
{
totalTokens
:
number
;
messageHistoryTokens
:
number
;
...
...
@@ -114,3 +115,9 @@ export interface TokenCountResult {
systemPromptTokens
:
number
;
contextWindow
:
number
;
}
export
interface
ChatLogsData
{
debugInfo
:
SystemDebugInfo
;
chat
:
Chat
;
codebase
:
string
;
}
src/preload.ts
浏览文件 @
19d1e890
...
...
@@ -14,6 +14,7 @@ const validInvokeChannels = [
"create-app"
,
"get-chat"
,
"get-chats"
,
"get-chat-logs"
,
"list-apps"
,
"get-app"
,
"edit-app-file"
,
...
...
@@ -53,6 +54,7 @@ const validInvokeChannels = [
"window:maximize"
,
"window:close"
,
"window:get-platform"
,
"upload-to-signed-url"
,
]
as
const
;
// Add valid receive channels
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论