mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 14:45:36 +00:00
## Summary This PR introduces the frontend error catalog display resolver as the foundation for the DES-220 / FE-816 error messaging work. The main goal is to create a single FE boundary where raw Core/Cloud error payloads can be converted into human-friendly display fields, while preserving the original API contract fields (`message` and `details`) unchanged. UI components can now prefer resolved display copy when it exists and fall back to the raw API copy otherwise. As a small concrete sample, this PR implements the first cataloged validation error: - `required_input_missing` is resolved as the `missing_connection` catalog item. - Panel title: `Missing connection` - Panel message: `Required input slots have no connection feeding them.` - Detail/item copy can include the node and input name, e.g. `KSampler is missing a required input: model` and `KSampler - model`. - Single-error toast/overlay-oriented fields are added to the data model for follow-up UI work, but this PR does not redesign the overlay. ## What This PR Targets This PR is intentionally scoped as the skeleton PR for the error catalog UX system. It adds: - A new resolver module under `src/platform/errorCatalog`. - Shared resolved display fields: - `catalogId` - `displayTitle` - `displayMessage` - `displayDetails` - `displayItemLabel` - `toastTitle` - `toastMessage` - A resolver entry point for run-time workflow errors: - node validation errors - execution/runtime errors - prompt errors - A resolver entry point for pre-run missing-resource groups: - missing node packs - swap nodes - missing models - missing media - Error group wiring so `useErrorGroups` resolves display copy in one place instead of making UI components own message decisions. - The first real validation rule for `required_input_missing` / Missing connection. - The existing prompt error copy moved into the `errorCatalog.promptErrors` namespace in `src/locales/en/main.json`. - Tests covering the resolver, grouping behavior, panel rendering, prompt error copy, missing group copy, and fallback behavior. ## What This PR Deliberately Does Not Target This PR avoids the larger UX and product behavior changes so the foundation can land separately. It does not: - Redesign the error overlay. - Redesign the right-side error panel. - Change the shape of Core/Cloud API error payloads. - Replace raw `message` / `details`; those remain intact for API-contract alignment and technical debugging. - Re-group execution errors by final message type yet. - Add special runtime error messaging for credits, timeouts, content policy, OOM, or rate limits. - Render the new `displayItemLabel` everywhere it will eventually be useful. ## User-Facing Behavior Most behavior is preserved. The main visible change is for missing required input validation errors. Those now display as Missing connection copy instead of exposing the raw validation message directly. Prompt errors should keep the same user-facing wording as before, but the source of that wording now lives under the error catalog namespace. Missing node/model/media/swap-node groups still preserve the existing titles, counts, and friendly messages, but their display copy now flows through the same resolver boundary. Execution/runtime errors receive catalog fields for future toast/overlay usage, but the current runtime overlay path intentionally keeps the raw technical error copy until the overlay redesign PR decides how to consume the new fields. ## Screenshots Before <img width="505" height="266" alt="스크린샷 2026-05-22 오후 2 15 27" src="https://github.com/user-attachments/assets/09e8eb31-dca4-42d8-8237-9474cb71a14c" /> <img width="463" height="317" alt="스크린샷 2026-05-22 오후 2 16 09" src="https://github.com/user-attachments/assets/c0a0159e-5bd9-4b3f-9c21-c0040373fbca" /> After <img width="482" height="297" alt="스크린샷 2026-05-22 오후 2 14 25" src="https://github.com/user-attachments/assets/4ca10bf0-29d2-4b65-940e-0d78db3fd278" /> <img width="460" height="194" alt="스크린샷 2026-05-22 오후 2 16 55" src="https://github.com/user-attachments/assets/20848054-5012-4dd3-b903-ef8c920f70c8" /> ## Follow-Up PR Plan This PR is the first stacked PR in the error catalog work. Follow-up PRs are expected to build on this foundation in roughly this order: 1. Expand general execution error messaging. - Add broader validation error handling beyond `required_input_missing`, including list/range/value validation cases. - Add general runtime execution messaging. - Continue migrating prompt error display decisions into the catalog resolver. 2. Add special runtime error messaging. - Credits / insufficient credits. - Timeout. - Content not allowed / blocked content. - Server crash. - Out of memory. - Rate limiting. - Other high-volume Cloud-only runtime failures from DES-220. 3. Re-group execution errors by message/catalog type. - Move away from grouping primarily by node class when the cataloged error type is the more useful user-facing grouping key. - Keep raw technical details available inside cards/logs. 4. Update the error overlay behavior. - Use `toastTitle` and `toastMessage` for single-error cases. - Use aggregate copy such as "N errors found" for multi-error cases. - Add node navigation affordances where appropriate. 5. Update the right-side error panel design. - Render resolved item labels such as `Node name - Input name`. - Align expanded card details and logs with the new design. - Preserve copy/debug affordances for technical details. 6. Fold in related missing media/model/node messaging improvements. - FE-583 should become a child/follow-up issue in this stack for improving missing image/media messaging. ## Validation - `pnpm format` - `pnpm lint` - `pnpm typecheck` - `pnpm test:unit` - Targeted resolver/grouping tests during review iterations - `pnpm knip` `pnpm knip` passes with only the pre-existing tag hint: `Unused tag in src/scripts/metadata/flac.ts: getFromFlacBuffer → @knipIgnoreUnusedButUsedByCustomNodes`
155 lines
4.8 KiB
Vue
155 lines
4.8 KiB
Vue
<template>
|
|
<Transition
|
|
enter-active-class="transition-all duration-300 ease-out"
|
|
enter-from-class="-translate-y-3 opacity-0"
|
|
enter-to-class="translate-y-0 opacity-100"
|
|
>
|
|
<div v-if="isVisible" class="pointer-events-none flex w-full justify-end">
|
|
<div
|
|
role="alert"
|
|
aria-live="assertive"
|
|
data-testid="error-overlay"
|
|
class="pointer-events-auto flex w-80 min-w-72 flex-col overflow-hidden rounded-lg border border-destructive-background bg-comfy-menu-bg shadow-interface transition-colors duration-200 ease-in-out"
|
|
>
|
|
<!-- Header -->
|
|
<div class="flex h-12 items-center gap-2 px-4">
|
|
<span class="flex-1 text-sm font-bold text-destructive-background">
|
|
{{ errorCountLabel }}
|
|
</span>
|
|
<Button
|
|
variant="muted-textonly"
|
|
size="icon-sm"
|
|
:aria-label="t('g.close')"
|
|
@click="dismiss"
|
|
>
|
|
<i class="icon-[lucide--x] block size-5 leading-none" />
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="px-4 pb-3" data-testid="error-overlay-messages">
|
|
<ul class="m-0 flex list-none flex-col gap-1.5 p-0">
|
|
<li
|
|
v-for="(message, idx) in overlayMessages"
|
|
:key="idx"
|
|
class="flex min-w-0 items-baseline gap-2 text-sm/snug text-muted-foreground"
|
|
>
|
|
<span
|
|
class="mt-1.5 size-1 shrink-0 rounded-full bg-muted-foreground"
|
|
/>
|
|
<span class="line-clamp-3 wrap-break-word whitespace-pre-wrap">{{
|
|
message
|
|
}}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="flex items-center justify-end gap-4 px-4 py-3">
|
|
<Button
|
|
variant="muted-textonly"
|
|
size="unset"
|
|
data-testid="error-overlay-dismiss"
|
|
@click="dismiss"
|
|
>
|
|
{{ t('g.dismiss') }}
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="lg"
|
|
data-testid="error-overlay-see-errors"
|
|
@click="seeErrors"
|
|
>
|
|
{{ appMode ? t('linearMode.error.goto') : seeErrorsLabel }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { storeToRefs } from 'pinia'
|
|
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
|
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
import { useErrorGroups } from '@/components/rightSidePanel/errors/useErrorGroups'
|
|
|
|
defineProps<{ appMode?: boolean }>()
|
|
|
|
const { t } = useI18n()
|
|
const executionErrorStore = useExecutionErrorStore()
|
|
const rightSidePanelStore = useRightSidePanelStore()
|
|
const canvasStore = useCanvasStore()
|
|
|
|
const { totalErrorCount, isErrorOverlayOpen } = storeToRefs(executionErrorStore)
|
|
const { allErrorGroups } = useErrorGroups(ref(''))
|
|
|
|
const singleErrorType = computed(() => {
|
|
const types = new Set(allErrorGroups.value.map((g) => g.type))
|
|
return types.size === 1 ? [...types][0] : null
|
|
})
|
|
|
|
const overlayMessages = computed<string[]>(() => {
|
|
const messages = new Set<string>()
|
|
for (const group of allErrorGroups.value) {
|
|
if (group.type === 'execution') {
|
|
// TODO(FE-816 overlay-redesign): Keep runtime overlay copy raw until the
|
|
// overlay redesign decides how to use catalog toast fields.
|
|
for (const card of group.cards) {
|
|
for (const err of card.errors) {
|
|
messages.add(err.message)
|
|
}
|
|
}
|
|
} else {
|
|
messages.add(group.displayMessage ?? group.displayTitle)
|
|
}
|
|
}
|
|
return Array.from(messages)
|
|
})
|
|
|
|
const seeErrorsLabel = computed(() => {
|
|
const labelMap: Record<string, string> = {
|
|
missing_node: t('errorOverlay.showMissingNodes'),
|
|
missing_model: t('errorOverlay.showMissingModels'),
|
|
swap_nodes: t('errorOverlay.showSwapNodes'),
|
|
missing_media: t('errorOverlay.showMissingMedia')
|
|
}
|
|
if (singleErrorType.value) {
|
|
return labelMap[singleErrorType.value] ?? t('errorOverlay.seeErrors')
|
|
}
|
|
return t('errorOverlay.seeErrors')
|
|
})
|
|
|
|
const errorCountLabel = computed(() =>
|
|
t(
|
|
'errorOverlay.errorCount',
|
|
{ count: totalErrorCount.value },
|
|
totalErrorCount.value
|
|
)
|
|
)
|
|
|
|
const isVisible = computed(
|
|
() => isErrorOverlayOpen.value && totalErrorCount.value > 0
|
|
)
|
|
|
|
function dismiss() {
|
|
executionErrorStore.dismissErrorOverlay()
|
|
}
|
|
|
|
function seeErrors() {
|
|
canvasStore.linearMode = false
|
|
if (canvasStore.canvas) {
|
|
canvasStore.canvas.deselectAll()
|
|
canvasStore.updateSelectedItems()
|
|
}
|
|
|
|
rightSidePanelStore.openPanel('errors')
|
|
executionErrorStore.dismissErrorOverlay()
|
|
}
|
|
</script>
|