mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 13:59:28 +00:00
## Summary Add detection and resolution UI for missing image/video/audio inputs (LoadImage, LoadVideo, LoadAudio nodes) in the Errors tab, mirroring the existing missing model pipeline. ## Changes - **What**: New `src/platform/missingMedia/` module — scan pipeline detects missing media files on workflow load (sync for OSS, async for cloud), surfaces them in the error tab with upload dropzone, thumbnail library select, and 2-step confirm flow - **Detection**: `scanAllMediaCandidates()` checks combo widget values against options; cloud path defers to `verifyCloudMediaCandidates()` via `assetsStore.updateInputs()` - **UI**: `MissingMediaCard` groups by media type; `MissingMediaRow` shows node name (single) or filename+count (multiple), upload dropzone with drag & drop, `MissingMediaLibrarySelect` with image/video thumbnails - **Resolution**: Upload via `/upload/image` API or select from library → status card → checkmark confirm → widget value applied, item removed from error list - **Integration**: `executionErrorStore` aggregates into `hasAnyError`/`totalErrorCount`; `useNodeErrorFlagSync` flags nodes on canvas; `useErrorGroups` renders in error tab - **Shared**: Extract `ACCEPTED_IMAGE_TYPES`/`ACCEPTED_VIDEO_TYPES` to `src/utils/mediaUploadUtil.ts`; extract `resolveComboValues` to `src/utils/litegraphUtil.ts` (shared across missingMedia + missingModel scan) - **Reverse clearing**: Widget value changes on nodes auto-remove corresponding missing media errors (via `clearWidgetRelatedErrors`) ## Testing ### Unit tests (22 tests) - `missingMediaScan.test.ts` (12): groupCandidatesByName, groupCandidatesByMediaType (ordering, multi-name), verifyCloudMediaCandidates (missing/present, abort before/after updateInputs, already resolved true/false, no-pending skip, updateInputs spy) - `missingMediaStore.test.ts` (10): setMissingMedia, clearMissingMedia (full lifecycle with interaction state), missingMediaNodeIds, hasMissingMediaOnNode, removeMissingMediaByWidget (match/no-match/last-entry), createVerificationAbortController ### E2E tests (10 scenarios in `missingMedia.spec.ts`) - Detection: error overlay shown, Missing Inputs group in errors tab, correct row count, dropzone + library select visibility, no false positive for valid media - Upload flow: file picker → uploading status card → confirm → row removed - Library select: dropdown → selected status card → confirm → row removed - Cancel: pending selection → returns to upload/library UI - All resolved: Missing Inputs group disappears - Locate node: canvas pans to missing media node ## Review Focus - Cloud verification path: `verifyCloudMediaCandidates` compares widget value against `asset_hash` — implicit contract - 2-step confirm mirrors missing model pattern (`pendingSelection` → confirm/cancel) - Event propagation guard on dropzone (`@drop.prevent.stop`) to prevent canvas LoadImage node creation - `clearAllErrors()` intentionally does NOT clear missing media (same as missing models — preserves pending repairs) - `runMissingMediaPipeline` is now `async` and `await`-ed, matching model pipeline ## Test plan - [x] OSS: load workflow with LoadImage referencing non-existent file → error tab shows it - [x] Upload file via dropzone → status card shows "Uploaded" → confirm → widget updated, error removed - [x] Select from library with thumbnail preview → confirm → widget updated, error removed - [x] Cancel pending selection → returns to upload/library UI - [x] Load workflow with valid images → no false positives - [x] Click locate-node → canvas navigates to the node - [x] Multiple nodes referencing different missing files → correct row count - [x] Widget value change on node → missing media error auto-removed ## Screenshots https://github.com/user-attachments/assets/631c0cb0-9706-4db2-8615-f24a4c3fe27d
101 lines
2.9 KiB
TypeScript
101 lines
2.9 KiB
TypeScript
import { computed, reactive, toValue, watch } from 'vue'
|
|
|
|
import type { MaybeRefOrGetter } from 'vue'
|
|
|
|
import { until } from '@vueuse/core'
|
|
|
|
import { api } from '@/scripts/api'
|
|
import { app } from '@/scripts/app'
|
|
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
|
import { generateErrorReport } from '@/utils/errorReportUtil'
|
|
|
|
import type { ErrorCardData } from './types'
|
|
|
|
/** Fallback exception type for error reports when the backend does not provide one. Not i18n'd: used in diagnostic reports only. */
|
|
const FALLBACK_EXCEPTION_TYPE = 'Runtime Error'
|
|
|
|
export function useErrorReport(cardSource: MaybeRefOrGetter<ErrorCardData>) {
|
|
const systemStatsStore = useSystemStatsStore()
|
|
const enrichedDetails = reactive<Record<number, string>>({})
|
|
|
|
const displayedDetailsMap = computed(() => {
|
|
const card = toValue(cardSource)
|
|
return Object.fromEntries(
|
|
card.errors.map((error, idx) => [
|
|
idx,
|
|
enrichedDetails[idx] ?? error.details
|
|
])
|
|
)
|
|
})
|
|
|
|
watch(
|
|
() => toValue(cardSource),
|
|
async (card, _, onCleanup) => {
|
|
let cancelled = false
|
|
onCleanup(() => {
|
|
cancelled = true
|
|
})
|
|
|
|
for (const key of Object.keys(enrichedDetails)) {
|
|
delete enrichedDetails[key as unknown as number]
|
|
}
|
|
|
|
const runtimeErrors = card.errors
|
|
.map((error, idx) => ({ error, idx }))
|
|
.filter(({ error }) => error.isRuntimeError)
|
|
|
|
if (runtimeErrors.length === 0) return
|
|
|
|
if (!systemStatsStore.systemStats) {
|
|
if (systemStatsStore.isLoading) {
|
|
await until(() => systemStatsStore.isLoading).toBe(false)
|
|
} else {
|
|
try {
|
|
await systemStatsStore.refetchSystemStats()
|
|
} catch (e) {
|
|
console.warn('Failed to fetch system stats for error report:', e)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if (!systemStatsStore.systemStats || cancelled) return
|
|
|
|
const logs = await api
|
|
.getLogs()
|
|
.catch(() => 'Failed to retrieve server logs')
|
|
if (cancelled) return
|
|
|
|
const workflow = (() => {
|
|
try {
|
|
return app.rootGraph.serialize()
|
|
} catch (e) {
|
|
console.warn('Failed to serialize workflow for error report:', e)
|
|
return null
|
|
}
|
|
})()
|
|
if (!workflow) return
|
|
|
|
for (const { error, idx } of runtimeErrors) {
|
|
try {
|
|
const report = generateErrorReport({
|
|
exceptionType: error.exceptionType ?? FALLBACK_EXCEPTION_TYPE,
|
|
exceptionMessage: error.message,
|
|
traceback: error.details,
|
|
nodeId: card.nodeId,
|
|
nodeType: card.title,
|
|
systemStats: systemStatsStore.systemStats,
|
|
serverLogs: logs,
|
|
workflow
|
|
})
|
|
enrichedDetails[idx] = report
|
|
} catch (e) {
|
|
console.warn('Failed to generate error report:', e)
|
|
}
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
return { displayedDetailsMap }
|
|
}
|