Execution Error Dialog Revamp (One click issue searching and filing) (#595)

* Add basic error dialog

* 2 level error report

* Add find issue button

* nit

* Add file issue button

* Single dialog

* nit

* Fix long text wrapping

* Merge component

* Test execution error dialog
This commit is contained in:
Chenlei Hu
2024-08-22 15:55:38 -04:00
committed by GitHub
parent 0466c79725
commit 3e457f812d
15 changed files with 410 additions and 36 deletions

View File

@@ -27,6 +27,11 @@ jobs:
with:
repository: "Comfy-Org/ComfyUI_frontend"
path: "ComfyUI_frontend"
- name: Checkout ComfyUI_devtools
uses: actions/checkout@v4
with:
repository: "Comfy-Org/ComfyUI_devtools"
path: "ComfyUI/custom_nodes/ComfyUI_devtools"
- name: Get commit message
id: commit-message
run: echo "::set-output name=message::$(git log -1 --pretty=%B)"

View File

@@ -124,6 +124,7 @@ export class ComfyPage {
// Buttons
public readonly resetViewButton: Locator
public readonly queueButton: Locator
// Inputs
public readonly workflowUploadInput: Locator
@@ -137,6 +138,7 @@ export class ComfyPage {
this.canvas = page.locator('#graph-canvas')
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
this.workflowUploadInput = page.locator('#comfy-file-input')
this.searchBox = new ComfyNodeSearchBox(page)
this.menu = new ComfyMenu(page)

View File

@@ -0,0 +1,82 @@
{
"last_node_id": 17,
"last_link_id": 15,
"nodes": [
{
"id": 14,
"type": "PreviewImage",
"pos": [
858,
-41
],
"size": {
"0": 213.8594970703125,
"1": 50.65289306640625
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 15
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 17,
"type": "DevToolsErrorRaiseNode",
"pos": [
477,
-40
],
"size": {
"0": 210,
"1": 26
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
15
],
"slot_index": 0,
"shape": 3
}
],
"properties": {
"Node name for S&R": "DevToolsErrorRaiseNode"
}
}
],
"links": [
[
15,
17,
0,
14,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.2100000000000006,
"offset": [
-266.1038310281165,
337.94335447664554
]
}
},
"version": 0.4
}

View File

@@ -12,3 +12,16 @@ test.describe('Load workflow warning', () => {
await expect(missingNodesWarning).toBeVisible()
})
})
test.describe('Execution error', () => {
test('Should display an error message when an execution error occurs', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
// Wait for the element with the .comfy-execution-error selector to be visible
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
})
})

46
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@primevue/themes": "^4.0.0-rc.2",
"@vitejs/plugin-vue": "^5.0.5",
"@vueuse/core": "^11.0.0",
"axios": "^1.7.4",
"class-transformer": "^0.5.1",
"dotenv": "^16.4.5",
"fuse.js": "^7.0.0",
@@ -4485,8 +4486,7 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
"version": "10.4.19",
@@ -4525,6 +4525,17 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -5155,7 +5166,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -5451,7 +5461,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@@ -6271,6 +6280,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
@@ -6303,7 +6332,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -9285,7 +9313,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -9294,7 +9321,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -10067,6 +10093,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",

View File

@@ -60,6 +60,7 @@
"@primevue/themes": "^4.0.0-rc.2",
"@vitejs/plugin-vue": "^5.0.5",
"@vueuse/core": "^11.0.0",
"axios": "^1.7.4",
"class-transformer": "^0.5.1",
"dotenv": "^16.4.5",
"fuse.js": "^7.0.0",

View File

@@ -44,6 +44,7 @@ defineEmits(['action'])
.no-results-placeholder :deep(.p-card) {
background-color: var(--surface-ground);
text-align: center;
box-shadow: unset;
}
.no-results-placeholder h3 {

View File

@@ -6,7 +6,7 @@
closable
closeOnEscape
dismissableMask
:maximizable="dialogStore.props.maximizable ?? false"
:maximizable="maximizable"
@hide="dialogStore.closeDialog"
@maximize="maximized = true"
@unmaximize="maximized = false"
@@ -19,20 +19,20 @@
<h3 v-else>{{ dialogStore.title || ' ' }}</h3>
</template>
<component
:is="dialogStore.component"
v-bind="dialogStore.props"
:maximized="maximized"
/>
<component :is="dialogStore.component" v-bind="contentProps" />
</Dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { useDialogStore } from '@/stores/dialogStore'
import Dialog from 'primevue/dialog'
const dialogStore = useDialogStore()
const maximizable = dialogStore.props.maximizable ?? false
const maximized = ref(false)
const contentProps = computed(() => ({
...dialogStore.props,
...(dialogStore.props.maximizable ? { maximized } : {})
}))
</script>

View File

@@ -1,5 +0,0 @@
<template>
<div></div>
</template>
<script setup lang="ts"></script>

View File

@@ -0,0 +1,182 @@
<template>
<NoResultsPlaceholder
icon="pi pi-exclamation-circle"
:title="props.error.node_type"
:message="props.error.exception_message"
/>
<div class="comfy-error-report">
<Button
v-show="!reportOpen"
:label="$t('showReport')"
@click="showReport"
text
/>
<template v-if="reportOpen">
<Divider />
<ScrollPanel style="width: 100%; height: 400px; max-width: 80vw">
<pre class="wrapper-pre">{{ reportContent }}</pre>
</ScrollPanel>
<Divider />
</template>
<div class="action-container">
<FindIssueButton
:errorMessage="props.error.exception_message"
:repoOwner="repoOwner"
:repoName="repoName"
/>
<Button
:label="$t('openNewIssue')"
icon="pi pi-github"
@click="openNewGithubIssue"
class="p-button-secondary"
/>
<Button
v-if="reportOpen"
:label="$t('copyToClipboard')"
icon="pi pi-copy"
@click="copyReportToClipboard"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useClipboard } from '@vueuse/core'
import { useToast } from 'primevue/usetoast'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ScrollPanel from 'primevue/scrollpanel'
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import type { ExecutionErrorWsMessage, SystemStats } from '@/types/apiTypes'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
const props = defineProps<{
error: ExecutionErrorWsMessage
}>()
const repoOwner = 'comfyanonymous'
const repoName = 'ComfyUI'
const reportContent = ref('')
const reportOpen = ref(false)
const showReport = () => {
reportOpen.value = true
}
const toast = useToast()
const { copy, isSupported } = useClipboard()
onMounted(async () => {
generateReport(await api.getSystemStats())
})
const generateReport = (systemStats: SystemStats) => {
const MAX_JSON_LENGTH = 50000
const workflowJSONString = JSON.stringify(app.graph.serialize())
const workflowText =
workflowJSONString.length > MAX_JSON_LENGTH
? 'Workflow too large. Please manually upload the workflow from local file system.'
: workflowJSONString
reportContent.value = `
# ComfyUI Error Report
## Error Details
- **Node Type:** ${props.error.node_type}
- **Exception Type:** ${props.error.exception_type}
- **Exception Message:** ${props.error.exception_message}
## Stack Trace
\`\`\`
${props.error.traceback.join('\n')}
\`\`\`
## System Information
- **OS:** ${systemStats.system.os}
- **Python Version:** ${systemStats.system.python_version}
- **Embedded Python:** ${systemStats.system.embedded_python}
## Devices
${systemStats.devices
.map(
(device) => `
- **Name:** ${device.name}
- **Type:** ${device.type}
- **VRAM Total:** ${device.vram_total}
- **VRAM Free:** ${device.vram_free}
- **Torch VRAM Total:** ${device.torch_vram_total}
- **Torch VRAM Free:** ${device.torch_vram_free}
`
)
.join('\n')}
## Attached Workflow
Please make sure that workflow does not contain any sensitive information such as API keys or passwords.
\`\`\`
${workflowText}
\`\`\`
## Additional Context
(Please add any additional context or steps to reproduce the error here)
`
}
const copyReportToClipboard = async () => {
if (isSupported) {
try {
await copy(reportContent.value)
toast.add({
severity: 'success',
summary: 'Success',
detail: 'Report copied to clipboard',
life: 3000
})
} catch (err) {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to copy report',
life: 3000
})
}
} else {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'Clipboard API not supported in your browser',
life: 3000
})
}
}
const openNewGithubIssue = () => {
const issueTitle = encodeURIComponent(
`[Bug]: ${props.error.exception_type} in ${props.error.node_type}`
)
const issueBody = encodeURIComponent(reportContent.value)
const url = `https://github.com/${repoOwner}/${repoName}/issues/new?title=${issueTitle}&body=${issueBody}`
window.open(url, '_blank')
}
</script>
<style scoped>
.comfy-error-report {
display: flex;
flex-direction: column;
gap: 1rem;
}
.action-container {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.wrapper-pre {
white-space: pre-wrap;
word-wrap: break-word;
}
.no-results-placeholder {
padding-top: 0;
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<Button
@click="openGitHubIssues"
:label="buttonLabel"
severity="secondary"
icon="pi pi-github"
:badge="issueCount.toString()"
>
</Button>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useAsyncState } from '@vueuse/core'
import axios from 'axios'
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
const props = defineProps<{
errorMessage: string
repoOwner: string
repoName: string
}>()
const GITHUB_API_URL = 'https://api.github.com/search/issues'
const queryString = computed(() => props.errorMessage + ' is:issue')
const getIssueCount = async () => {
const query = `${queryString.value} repo:${props.repoOwner}/${props.repoName}`
const response = await axios.get(GITHUB_API_URL, {
params: {
q: query,
per_page: 1
}
})
return response.data.total_count
}
const {
state: issueCount,
isLoading,
execute
} = useAsyncState(getIssueCount, 0)
const { t } = useI18n()
const buttonLabel = computed(() => {
return isLoading.value ? 'Loading...' : t('findIssues')
})
const openGitHubIssues = () => {
const query = encodeURIComponent(queryString.value)
const url = `https://github.com/${props.repoOwner}/${props.repoName}/issues?q=${query}`
window.open(url, '_blank')
}
</script>

View File

@@ -2,6 +2,10 @@ import { createI18n } from 'vue-i18n'
const messages = {
en: {
findIssues: 'Find Issues',
copyToClipboard: 'Copy to Clipboard',
openNewIssue: 'Open New Issue',
showReport: 'Show Report',
imageFailedToLoad: 'Image failed to load',
reconnecting: 'Reconnecting',
reconnected: 'Reconnected',
@@ -31,6 +35,7 @@ const messages = {
}
},
zh: {
showReport: '显示报告',
imageFailedToLoad: '图像加载失败',
reconnecting: '重新连接中',
reconnected: '已重新连接',

View File

@@ -43,7 +43,10 @@ import {
} from '@/stores/nodeDefStore'
import { Vector2 } from '@comfyorg/litegraph'
import _ from 'lodash'
import { showLoadWorkflowWarning } from '@/services/dialogService'
import {
showExecutionErrorDialog,
showLoadWorkflowWarning
} from '@/services/dialogService'
import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import type { ToastMessageOptions } from 'primevue/toast'
@@ -1666,8 +1669,7 @@ export class ComfyApp {
api.addEventListener('execution_error', ({ detail }) => {
this.lastExecutionError = detail
const formattedError = this.#formatExecutionError(detail)
this.ui.dialog.show(formattedError)
showExecutionErrorDialog(detail)
this.canvas.draw(true, true)
})
@@ -2552,18 +2554,6 @@ export class ComfyApp {
return '(unknown error)'
}
#formatExecutionError(error) {
if (error == null) {
return '(unknown error)'
}
const traceback = error.traceback.join('')
const nodeId = error.node_id
const nodeType = error.node_type
return `Error occurred when executing ${nodeType}:\n\n${error.exception_message}\n\n${traceback}`
}
async queuePrompt(number, batchCount = 1) {
this.#queueItems.push({ number, batchCount })

View File

@@ -5,6 +5,8 @@ import { useDialogStore } from '@/stores/dialogStore'
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import type { ExecutionErrorWsMessage } from '@/types/apiTypes'
import ExecutionErrorDialogContent from '@/components/dialog/content/ExecutionErrorDialogContent.vue'
export function showLoadWorkflowWarning(props: {
missingNodeTypes: any[]
@@ -24,3 +26,12 @@ export function showSettingsDialog() {
component: SettingDialogContent
})
}
export function showExecutionErrorDialog(error: ExecutionErrorWsMessage) {
useDialogStore().showDialog({
component: ExecutionErrorDialogContent,
props: {
error
}
})
}

View File

@@ -68,7 +68,7 @@ const zExecutionErrorWsMessage = zExecutionWsMessageBase.extend({
executed: z.array(zNodeId),
exception_message: z.string(),
exception_type: z.string(),
traceback: z.string(),
traceback: z.array(z.string()),
current_inputs: z.any(),
current_outputs: z.any()
})