mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-08 23:39:23 +00:00
## Summary This is the first PR in a planned stack to modernize the Workflow Overview error tab. It focuses only on execution-style errors: validation errors, runtime errors, and known prompt errors. The intent is to establish the catalog-driven presentation model before touching the larger missing-resource cards. Validation and prompt errors are known product states, so this PR makes them read more like structured guidance instead of generic reportable failures. Runtime errors remain reportable, but their details are reorganized so the error log is easier to scan and copy. ## What changed - Groups validation errors by error catalog id instead of node class/type. - Adds an `unknown_validation_error` fallback catalog id so validation grouping can follow one rule without special-case missing catalog ids. - Shows validation group title and message once, then lists each affected input as a compact item row. - Adds per-item validation detail disclosure so detailed validation text is still available without repeating the group title/message for every item. - Keeps locate-node behavior available from validation rows, including keyboard/ARIA disclosure wiring. - Removes GitHub, copy, and help actions from validation/prompt errors because these are known, cataloged errors where the UI copy should guide the user directly. - Refines runtime error cards so the error log is visible by default, has its own header, and keeps copy/report actions inside the log area. - Removes the special full-panel singleton runtime layout so runtime errors keep the same fixed card rhythm as the other error groups. - Keeps runtime errors reportable via Get Help and Find on GitHub, because these can still represent unexpected execution failures. - Updates prompt error detail styling to match the darker runtime error-log treatment. - Restores display-message semantics for grouped execution messages: `displayMessage ?? message` is used for user-facing dedupe instead of raw backend-only messages. - Adds focused unit coverage for catalog grouping, fallback validation catalog ids, display-message grouping, runtime detail behavior, and the updated prompt/validation action model. ## Planned stack This PR intentionally keeps the first slice narrow. The broader redesign is planned as a sequence of follow-up PRs rather than one large change: 1. Execution errors, this PR: validation, runtime, and prompt error grouping/presentation. 2. Missing media: simplify image/video/audio missing-media cards around catalog item labels and locate actions. 3. Missing node and swap node: align missing-pack rows, nested node references, install/replace actions, and locate behavior. 4. Missing model: unify OSS and Cloud presentation, simplify download/import actions, and improve import/download progress behavior. The goal is to review and stabilize each slice before stacking the next one. This is especially important because later missing-model changes are much larger and should not obscure the catalog/error-card behavior introduced here. ## Review focus - Validation errors should now group by catalog id, not by node class. - Validation groups intentionally show one message per group, with individual affected inputs rendered as rows. - Prompt and validation errors intentionally no longer show report/copy/help actions. - Runtime errors intentionally still show report actions, but only inside the error-log panel. - Node id badges are intentionally not shown in these execution error rows; the follow-up missing-resource PRs will handle their own row treatments separately. - This PR does not change missing media, missing model, missing node pack, or swap node cards. ## Screenshots ### This PR Validation error <img width="457" height="362" alt="스크린샷 2026-06-07 오전 4 26 19" src="https://github.com/user-attachments/assets/4c35b9f3-57dd-4dae-b44a-6d2fd8547b7c" /> Runtime error <img width="454" height="545" alt="스크린샷 2026-06-07 오전 4 24 24" src="https://github.com/user-attachments/assets/b7d4482f-b35b-4ed2-90f2-0a62dafa3519" /> Prompt / Service error <img width="456" height="192" alt="스크린샷 2026-06-07 오전 4 27 58" src="https://github.com/user-attachments/assets/aeec0978-b47f-40c7-ab71-0a0d18ceb054" /> ### Old (main) Validation error <img width="457" height="853" alt="스크린샷 2026-06-07 오전 4 25 09" src="https://github.com/user-attachments/assets/185dd573-430d-4041-8b31-a8eb6346f1ff" /> Runtime error <img width="455" height="554" alt="스크린샷 2026-06-07 오전 4 24 58" src="https://github.com/user-attachments/assets/deb1c09d-ea58-4d6a-9ac6-d2a3a9832fbe" /> Prompt / Service error <img width="455" height="297" alt="스크린샷 2026-06-07 오전 4 28 14" src="https://github.com/user-attachments/assets/c68eef7c-6525-4a5b-858c-6482fe76ad27" /> ## Validation - `pnpm format:check` - `pnpm test:unit src/components/rightSidePanel/errors/TabErrors.test.ts src/components/rightSidePanel/errors/ErrorNodeCard.test.ts src/components/rightSidePanel/errors/useErrorGroups.test.ts src/platform/errorCatalog/errorMessageResolver.test.ts` - `pnpm typecheck` - `pnpm lint` - `pnpm knip` - `pnpm build`
272 lines
9.2 KiB
Vue
272 lines
9.2 KiB
Vue
<template>
|
|
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
<div
|
|
v-if="card.nodeId && !compact"
|
|
class="flex flex-wrap items-center gap-2 py-2"
|
|
>
|
|
<button
|
|
v-if="hasRuntimeError && (card.nodeTitle || card.title)"
|
|
type="button"
|
|
class="m-0 min-w-0 flex-1 cursor-pointer appearance-none truncate border-0 bg-transparent p-0 text-left text-sm font-medium text-muted-foreground outline-none hover:text-base-foreground focus:outline-none focus-visible:underline focus-visible:ring-0 focus-visible:outline-none"
|
|
@click="handleLocateNode"
|
|
>
|
|
{{ card.nodeTitle || card.title }}
|
|
</button>
|
|
<span
|
|
v-else-if="card.nodeTitle || card.title"
|
|
class="flex-1 truncate text-sm font-medium text-muted-foreground"
|
|
>
|
|
{{ card.nodeTitle || card.title }}
|
|
</span>
|
|
<div class="flex shrink-0 items-center">
|
|
<Button
|
|
v-if="card.isSubgraphNode"
|
|
variant="secondary"
|
|
size="sm"
|
|
class="h-8 shrink-0 rounded-lg text-sm"
|
|
@click.stop="handleEnterSubgraph"
|
|
>
|
|
{{ t('rightSidePanel.enterSubgraph') }}
|
|
</Button>
|
|
<Button
|
|
v-if="hasRuntimeError"
|
|
variant="textonly"
|
|
size="icon-sm"
|
|
:class="
|
|
cn(
|
|
'size-8 shrink-0 text-muted-foreground hover:text-base-foreground',
|
|
runtimeDetailsExpanded &&
|
|
'bg-secondary-background-selected text-base-foreground hover:bg-secondary-background-selected'
|
|
)
|
|
"
|
|
:aria-label="t('g.details')"
|
|
:aria-controls="runtimeDetailsControlIds || undefined"
|
|
:aria-expanded="runtimeDetailsExpanded"
|
|
@click.stop="toggleRuntimeDetails"
|
|
>
|
|
<i class="icon-[lucide--monitor-x] size-4" />
|
|
</Button>
|
|
<Button
|
|
variant="textonly"
|
|
size="icon-sm"
|
|
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground"
|
|
:aria-label="t('rightSidePanel.locateNode')"
|
|
@click.stop="handleLocateNode"
|
|
>
|
|
<i class="icon-[lucide--locate] size-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="flex min-h-0 flex-1 flex-col space-y-4 divide-y divide-interface-stroke/20"
|
|
>
|
|
<div
|
|
v-for="(error, idx) in card.errors"
|
|
:key="idx"
|
|
class="flex min-h-0 flex-col gap-3"
|
|
>
|
|
<p
|
|
v-if="getInlineMessage(error)"
|
|
class="m-0 max-h-[4lh] overflow-y-auto px-0.5 text-sm/relaxed wrap-break-word whitespace-pre-wrap"
|
|
>
|
|
{{ getInlineMessage(error) }}
|
|
</p>
|
|
|
|
<ul
|
|
v-if="getInlineItemLabel(error)"
|
|
class="m-0 list-disc space-y-1 pl-5 text-sm/relaxed text-muted-foreground marker:text-muted-foreground"
|
|
>
|
|
<li class="min-w-0 wrap-break-word">
|
|
<button
|
|
v-if="card.nodeId"
|
|
type="button"
|
|
class="m-0 inline max-w-full cursor-pointer appearance-none border-0 bg-transparent p-0 text-left text-sm/relaxed font-normal wrap-break-word text-muted-foreground outline-none hover:text-base-foreground focus:outline-none focus-visible:underline focus-visible:ring-0 focus-visible:outline-none"
|
|
@click="handleLocateNode"
|
|
>
|
|
{{ getInlineItemLabel(error) }}
|
|
</button>
|
|
<span v-else>
|
|
{{ getInlineItemLabel(error) }}
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
|
|
<div
|
|
v-if="!error.isRuntimeError && getInlineDetails(error, idx)"
|
|
:class="
|
|
cn(
|
|
'overflow-y-auto rounded-lg border border-interface-stroke/30 bg-secondary-background p-2.5',
|
|
'max-h-[6lh]'
|
|
)
|
|
"
|
|
>
|
|
<p
|
|
class="m-0 font-mono text-xs/relaxed wrap-break-word whitespace-pre-wrap text-muted-foreground"
|
|
>
|
|
{{ getInlineDetails(error, idx) }}
|
|
</p>
|
|
</div>
|
|
|
|
<TransitionCollapse>
|
|
<div
|
|
v-if="error.isRuntimeError && isRuntimeDisclosureExpanded"
|
|
:id="getRuntimeDetailsId(idx)"
|
|
role="region"
|
|
data-testid="runtime-error-panel"
|
|
:aria-label="t('rightSidePanel.errorLog')"
|
|
class="flex min-h-0 flex-col gap-3"
|
|
>
|
|
<div
|
|
v-if="getInlineDetails(error, idx)"
|
|
class="overflow-hidden rounded-lg border border-interface-stroke/30 bg-secondary-background"
|
|
>
|
|
<div
|
|
class="flex items-center justify-between gap-2 px-3 pt-3 pb-2"
|
|
>
|
|
<span
|
|
class="text-xs font-semibold tracking-wide text-base-foreground uppercase"
|
|
>
|
|
{{ t('rightSidePanel.errorLog') }}
|
|
</span>
|
|
<Button
|
|
variant="textonly"
|
|
size="icon-sm"
|
|
class="size-7 shrink-0 text-muted-foreground hover:text-base-foreground"
|
|
:aria-label="t('g.copy')"
|
|
data-testid="error-card-copy"
|
|
@click="handleCopyError(idx)"
|
|
>
|
|
<i class="icon-[lucide--copy] size-4" />
|
|
</Button>
|
|
</div>
|
|
<div class="max-h-[15lh] overflow-y-auto px-3 pb-3">
|
|
<p
|
|
class="m-0 font-mono text-xs/relaxed wrap-break-word whitespace-pre-wrap text-muted-foreground"
|
|
>
|
|
{{ getInlineDetails(error, idx) }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="mx-3 mt-1 h-px bg-base-foreground/20" />
|
|
<div class="mx-3 flex items-center justify-between gap-2 py-2">
|
|
<Button
|
|
v-tooltip.top="t('rightSidePanel.getHelpTooltip')"
|
|
variant="textonly"
|
|
size="sm"
|
|
class="h-8 justify-start gap-1 rounded-lg px-0 text-sm hover:bg-transparent hover:text-base-foreground"
|
|
@click="handleGetHelp"
|
|
>
|
|
<i class="icon-[lucide--external-link] size-3.5" />
|
|
{{ t('g.getHelpAction') }}
|
|
</Button>
|
|
<Button
|
|
v-tooltip.top="t('rightSidePanel.findOnGithubTooltip')"
|
|
variant="textonly"
|
|
size="sm"
|
|
class="h-8 justify-end gap-1 rounded-lg px-0 text-sm hover:bg-transparent hover:text-base-foreground"
|
|
data-testid="error-card-find-on-github"
|
|
@click="handleCheckGithub(error)"
|
|
>
|
|
<i class="icon-[lucide--github] size-3.5" />
|
|
{{ t('g.findOnGithub') }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TransitionCollapse>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import { cn } from '@comfyorg/tailwind-utils'
|
|
import TransitionCollapse from '../layout/TransitionCollapse.vue'
|
|
|
|
import type { ErrorCardData, ErrorItem } from './types'
|
|
import { useErrorActions } from './useErrorActions'
|
|
import { useErrorReport } from './useErrorReport'
|
|
|
|
const { card, compact = false } = defineProps<{
|
|
card: ErrorCardData
|
|
compact?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
locateNode: [nodeId: string]
|
|
enterSubgraph: [nodeId: string]
|
|
copyToClipboard: [text: string]
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const { displayedDetailsMap } = useErrorReport(() => card)
|
|
const { findOnGitHub, contactSupport: handleGetHelp } = useErrorActions()
|
|
const runtimeDetailsExpanded = ref(true)
|
|
const hasRuntimeError = computed(() =>
|
|
card.errors.some((error) => error.isRuntimeError)
|
|
)
|
|
const isRuntimeDisclosureExpanded = computed(
|
|
() => compact || runtimeDetailsExpanded.value
|
|
)
|
|
const runtimeDetailsControlIds = computed(() =>
|
|
card.errors
|
|
.map((error, idx) => (error.isRuntimeError ? getRuntimeDetailsId(idx) : ''))
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
)
|
|
|
|
function toggleRuntimeDetails() {
|
|
runtimeDetailsExpanded.value = !runtimeDetailsExpanded.value
|
|
}
|
|
|
|
function handleLocateNode() {
|
|
if (card.nodeId) {
|
|
emit('locateNode', card.nodeId)
|
|
}
|
|
}
|
|
|
|
function handleEnterSubgraph() {
|
|
if (card.nodeId) {
|
|
emit('enterSubgraph', card.nodeId)
|
|
}
|
|
}
|
|
|
|
function handleCopyError(idx: number) {
|
|
const details = displayedDetailsMap.value[idx]
|
|
const message = getCopyMessage(card.errors[idx])
|
|
emit('copyToClipboard', [message, details].filter(Boolean).join('\n\n'))
|
|
}
|
|
|
|
function handleCheckGithub(error: ErrorItem) {
|
|
findOnGitHub(error.message)
|
|
}
|
|
|
|
function getCopyMessage(error: ErrorItem | undefined) {
|
|
return error?.displayMessage ?? error?.message
|
|
}
|
|
|
|
function getInlineMessage(error: ErrorItem | undefined) {
|
|
if (!error || error.displayMessage) return undefined
|
|
return error.message
|
|
}
|
|
|
|
function getInlineItemLabel(error: ErrorItem | undefined) {
|
|
if (!error || error.isRuntimeError) return undefined
|
|
return error.displayItemLabel
|
|
}
|
|
|
|
function getInlineDetails(error: ErrorItem | undefined, idx: number) {
|
|
if (getInlineItemLabel(error)) return undefined
|
|
return displayedDetailsMap.value[idx]
|
|
}
|
|
|
|
function getRuntimeDetailsId(idx: number) {
|
|
return `${card.id}-runtime-details-${idx}`
|
|
}
|
|
</script>
|