mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-01 11:42:06 +00:00
## Summary When a workflow is loaded with missing models, users currently have no way to identify or resolve them from within the UI. This PR adds a full missing-model detection and resolution pipeline that surfaces missing models in the Errors tab, allowing users to install or import them without leaving the editor. ## Changes ### Missing Model Detection - Scan all COMBO widgets across root graph and subgraphs for model-like filenames during workflow load - Enrich candidates with embedded workflow metadata (url, hash, directory) when available - Verify asset-supported candidates against the asset store asynchronously to confirm installation status - Propagate missing model state to `executionErrorStore` alongside existing node/prompt errors ### Errors Tab UI — Model Resolution - Group missing models by directory (e.g. `checkpoints`, `loras`, `vae`) with collapsible category cards - Each model row displays: - Model name with copy-to-clipboard button - Expandable list of referencing nodes with locate-on-canvas button - **Library selector**: Pick an alternative from the user's existing models to substitute the missing model with one click - **URL import**: Paste a Civitai or HuggingFace URL to import a model directly; debounced metadata fetch shows filename and file size before confirming; type-mismatch warnings (e.g. importing a LoRA into checkpoints directory) are surfaced with an "Import Anyway" option - **Upgrade prompt**: In cloud environment, free-tier subscribers are shown an upgrade modal when attempting URL import - Separate "Import Not Supported" section for custom-node models that cannot be auto-resolved - Status card with live download progress, completion, failure, and category-mismatch states ### Canvas Integration - Highlight nodes and widgets that reference missing models with error indicators - Propagate missing-model badges through subgraph containers so issues are visible at every graph level ### Code Cleanup - Simplify `surfacePendingWarnings` in workflowService, remove stale widget-detected model merging logic - Add `flattenWorkflowNodes` utility to workflowSchema for traversing nested subgraph structures - Extract `MissingModelUrlInput`, `MissingModelLibrarySelect`, `MissingModelStatusCard` as focused single-responsibility components ## Testing - Unit tests for scan pipeline (`missingModelScan.test.ts`): enrichment, skip-installed, subgraph flattening - Unit tests for store (`missingModelStore.test.ts`): state management, removal helpers - Unit tests for interactions (`useMissingModelInteractions.test.ts`): combo select, URL input, import flow, library confirm - Component tests for `MissingModelCard` and error grouping (`useErrorGroups.test.ts`) - Updated `workflowService.test.ts` and `workflowSchema.test.ts` for new logic ## Review Focus - Missing model scan + enrichment pipeline in `missingModelScan.ts` - Interaction composable `useMissingModelInteractions.ts` — URL metadata fetch, library install, upload fallback - Store integration and canvas-level error propagation ## Screenshots https://github.com/user-attachments/assets/339a6d5b-93a3-43cd-98dd-0fb00681b66f ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9743-feat-Surface-missing-models-in-Errors-tab-Cloud-3206d73d365081678326d3a16c2165d8) by [Unito](https://www.unito.io)
157 lines
5.0 KiB
Vue
157 lines
5.0 KiB
Vue
<template>
|
|
<div
|
|
:class="
|
|
cn(
|
|
'flex h-8 items-center rounded-lg border border-transparent bg-secondary-background px-3 transition-colors focus-within:border-interface-stroke',
|
|
!canImportModels && 'cursor-pointer'
|
|
)
|
|
"
|
|
v-bind="upgradePromptAttrs"
|
|
@click="!canImportModels && showUploadDialog()"
|
|
>
|
|
<label :for="`url-input-${modelKey}`" class="sr-only">
|
|
{{ t('rightSidePanel.missingModels.urlPlaceholder') }}
|
|
</label>
|
|
<input
|
|
:id="`url-input-${modelKey}`"
|
|
type="text"
|
|
:value="urlInputs[modelKey] ?? ''"
|
|
:readonly="!canImportModels"
|
|
:placeholder="t('rightSidePanel.missingModels.urlPlaceholder')"
|
|
:class="
|
|
cn(
|
|
'text-foreground w-full border-none bg-transparent text-xs outline-none placeholder:text-muted-foreground',
|
|
!canImportModels && 'pointer-events-none opacity-60'
|
|
)
|
|
"
|
|
@input="
|
|
handleUrlInput(modelKey, ($event.target as HTMLInputElement).value)
|
|
"
|
|
/>
|
|
<Button
|
|
v-if="urlInputs[modelKey]"
|
|
variant="textonly"
|
|
size="icon-sm"
|
|
:aria-label="t('rightSidePanel.missingModels.clearUrl')"
|
|
class="ml-1 shrink-0"
|
|
@click.stop="handleUrlInput(modelKey, '')"
|
|
>
|
|
<i aria-hidden="true" class="icon-[lucide--x] size-3.5" />
|
|
</Button>
|
|
</div>
|
|
|
|
<TransitionCollapse>
|
|
<div v-if="urlMetadata[modelKey]" class="flex flex-col gap-2">
|
|
<div class="flex items-center gap-2 px-0.5 pt-0.5">
|
|
<span class="text-foreground min-w-0 truncate text-xs font-bold">
|
|
{{ urlMetadata[modelKey]?.filename }}
|
|
</span>
|
|
<span
|
|
v-if="(urlMetadata[modelKey]?.content_length ?? 0) > 0"
|
|
class="shrink-0 rounded-sm bg-secondary-background-selected px-1.5 py-0.5 text-xs font-medium text-muted-foreground"
|
|
>
|
|
{{ formatSize(urlMetadata[modelKey]?.content_length ?? 0) }}
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="typeMismatch" class="flex items-start gap-1.5 px-0.5">
|
|
<i
|
|
aria-hidden="true"
|
|
class="mt-0.5 icon-[lucide--triangle-alert] size-3 shrink-0 text-warning-background"
|
|
/>
|
|
<span class="text-xs/tight text-warning-background">
|
|
{{
|
|
t('rightSidePanel.missingModels.typeMismatch', {
|
|
detectedType: typeMismatch
|
|
})
|
|
}}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="pt-0.5">
|
|
<Button
|
|
variant="primary"
|
|
class="h-9 w-full justify-center gap-2 text-sm font-semibold"
|
|
:loading="urlImporting[modelKey]"
|
|
@click="handleImport(modelKey, directory)"
|
|
>
|
|
<i aria-hidden="true" class="icon-[lucide--download] size-4" />
|
|
{{
|
|
typeMismatch
|
|
? t('rightSidePanel.missingModels.importAnyway')
|
|
: t('rightSidePanel.missingModels.import')
|
|
}}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</TransitionCollapse>
|
|
|
|
<TransitionCollapse>
|
|
<div
|
|
v-if="urlFetching[modelKey]"
|
|
aria-live="polite"
|
|
class="flex items-center justify-center py-2"
|
|
>
|
|
<i
|
|
aria-hidden="true"
|
|
class="icon-[lucide--loader-circle] size-4 animate-spin text-muted-foreground"
|
|
/>
|
|
<span class="sr-only">{{ t('g.loading') }}</span>
|
|
</div>
|
|
</TransitionCollapse>
|
|
|
|
<TransitionCollapse>
|
|
<div v-if="urlErrors[modelKey]" class="px-0.5" role="alert">
|
|
<span class="text-xs text-destructive-background-hover">
|
|
{{ urlErrors[modelKey] }}
|
|
</span>
|
|
</div>
|
|
</TransitionCollapse>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { storeToRefs } from 'pinia'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import TransitionCollapse from '@/components/rightSidePanel/layout/TransitionCollapse.vue'
|
|
import { formatSize } from '@/utils/formatUtil'
|
|
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
|
import { useMissingModelInteractions } from '@/platform/missingModel/composables/useMissingModelInteractions'
|
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
|
import { useModelUpload } from '@/platform/assets/composables/useModelUpload'
|
|
|
|
const { modelKey, directory, typeMismatch } = defineProps<{
|
|
modelKey: string
|
|
directory: string | null
|
|
typeMismatch: string | null
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const { flags } = useFeatureFlags()
|
|
const canImportModels = computed(() => flags.privateModelsEnabled)
|
|
const { showUploadDialog } = useModelUpload()
|
|
|
|
const store = useMissingModelStore()
|
|
const { urlInputs, urlMetadata, urlFetching, urlErrors, urlImporting } =
|
|
storeToRefs(store)
|
|
|
|
const { handleUrlInput, handleImport } = useMissingModelInteractions()
|
|
|
|
const upgradePromptAttrs = computed(() =>
|
|
canImportModels.value
|
|
? {}
|
|
: {
|
|
role: 'button',
|
|
tabindex: 0,
|
|
onKeydown: (e: KeyboardEvent) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault()
|
|
showUploadDialog()
|
|
}
|
|
}
|
|
}
|
|
)
|
|
</script>
|