mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
[refactor] Redesign missing models dialog (#9014)
## Summary Redesign the missing models warning dialog to match the MissingNodes dialog pattern with header/content/footer separation, type badges, file sizes, and context-sensitive actions. ## Changes - **What**: Split `MissingModelsWarning.vue` into `MissingModelsHeader`, `MissingModelsContent`, `MissingModelsFooter` components following the established MissingNodes pattern. Added model type badges (VAE, DIFFUSION, LORA, etc.), inline file sizes, total download size, custom model warnings, and context-sensitive footer buttons (Download all / Download available / Ok, got it). Extracted security validation into shared `missingModelsUtils.ts`. Removed orphaned `FileDownload`, `ElectronFileDownload`, `useDownload`, and `useCivitaiModel` files. - **Breaking**: None ## Review Focus - Badge styling and icon button variants for theme compatibility - Security validation logic preserved correctly in extracted utility - E2e test locator updates for the new dialog structure <img width="641" height="478" alt="스크린샷 2026-02-20 오후 7 35 23" src="https://github.com/user-attachments/assets/ded27dc7-04e6-431d-9b2e-a96ba61043a4" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9014-refactor-Redesign-missing-models-dialog-30d6d73d365081809cb0c555c2c28034) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -1,95 +0,0 @@
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
type ModelType =
|
||||
| 'Checkpoint'
|
||||
| 'TextualInversion'
|
||||
| 'Hypernetwork'
|
||||
| 'AestheticGradient'
|
||||
| 'LORA'
|
||||
| 'Controlnet'
|
||||
| 'Poses'
|
||||
|
||||
interface CivitaiFileMetadata {
|
||||
fp?: 'fp16' | 'fp32'
|
||||
size?: 'full' | 'pruned'
|
||||
format?: 'SafeTensor' | 'PickleTensor' | 'Other'
|
||||
}
|
||||
|
||||
interface CivitaiModelFile {
|
||||
name: string
|
||||
id: number
|
||||
sizeKB: number
|
||||
type: string
|
||||
downloadUrl: string
|
||||
metadata: CivitaiFileMetadata
|
||||
}
|
||||
|
||||
interface CivitaiModel {
|
||||
name: string
|
||||
type: ModelType
|
||||
}
|
||||
|
||||
interface CivitaiModelVersionResponse {
|
||||
id: number
|
||||
name: string
|
||||
model: CivitaiModel
|
||||
modelId: number
|
||||
files: CivitaiModelFile[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable to manage Civitai model
|
||||
* @param url - The URL of the Civitai model, where the model ID is the last part of the URL's pathname
|
||||
* @see https://developer.civitai.com/docs/api/public-rest
|
||||
* @example
|
||||
* const { fileSize, isLoading, error, modelData } =
|
||||
* useCivitaiModel('https://civitai.com/api/download/models/16576?type=Model&format=SafeTensor&size=full&fp=fp16')
|
||||
*/
|
||||
export function useCivitaiModel(url: string) {
|
||||
const createModelVersionUrl = (modelId: string): string =>
|
||||
`https://civitai.com/api/v1/model-versions/${modelId}`
|
||||
|
||||
const extractModelIdFromUrl = (): string | null => {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.pathname.split('/').pop() || null
|
||||
}
|
||||
|
||||
const fetchModelData =
|
||||
async (): Promise<CivitaiModelVersionResponse | null> => {
|
||||
const modelId = extractModelIdFromUrl()
|
||||
if (!modelId) return null
|
||||
|
||||
const apiUrl = createModelVersionUrl(modelId)
|
||||
const res = await fetch(apiUrl)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
const findMatchingFileSize = (): number | null => {
|
||||
const matchingFile = modelData.value?.files?.find(
|
||||
(file) => file.downloadUrl && url.startsWith(file.downloadUrl)
|
||||
)
|
||||
|
||||
return matchingFile?.sizeKB ? matchingFile.sizeKB << 10 : null
|
||||
}
|
||||
|
||||
const {
|
||||
state: modelData,
|
||||
isLoading,
|
||||
error
|
||||
} = useAsyncState(fetchModelData, null, {
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const fileSize = computed(() =>
|
||||
!isLoading.value ? findMatchingFileSize() : null
|
||||
)
|
||||
|
||||
return {
|
||||
fileSize,
|
||||
isLoading,
|
||||
error,
|
||||
modelData
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { useCivitaiModel } from '@/composables/useCivitaiModel'
|
||||
import { downloadUrlToHfRepoUrl, isCivitaiModelUrl } from '@/utils/formatUtil'
|
||||
|
||||
export function useDownload(url: string, fileName?: string) {
|
||||
const fileSize = ref<number | null>(null)
|
||||
const error = ref<Error | null>(null)
|
||||
|
||||
const setFileSize = (size: number) => {
|
||||
fileSize.value = size
|
||||
}
|
||||
|
||||
const fetchFileSize = async () => {
|
||||
try {
|
||||
const response = await fetch(url, { method: 'HEAD' })
|
||||
if (!response.ok) throw new Error('Failed to fetch file size')
|
||||
|
||||
const size = response.headers.get('content-length')
|
||||
if (size) {
|
||||
setFileSize(parseInt(size))
|
||||
} else {
|
||||
console.error('"content-length" header not found')
|
||||
return null
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching file size:', e)
|
||||
error.value = e instanceof Error ? e : new Error(String(e))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger browser download
|
||||
*/
|
||||
const triggerBrowserDownload = () => {
|
||||
const link = document.createElement('a')
|
||||
if (url.includes('huggingface.co') && error.value) {
|
||||
// If model is a gated HF model, send user to the repo page so they can sign in first
|
||||
link.href = downloadUrlToHfRepoUrl(url)
|
||||
} else {
|
||||
link.href = url
|
||||
link.download = fileName || url.split('/').pop() || 'download'
|
||||
}
|
||||
link.target = '_blank' // Opens in new tab if download attribute is not supported
|
||||
link.rel = 'noopener noreferrer' // Security best practice for _blank links
|
||||
link.click()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isCivitaiModelUrl(url)) {
|
||||
const { fileSize: civitaiSize, error: civitaiErr } = useCivitaiModel(url)
|
||||
whenever(civitaiSize, setFileSize)
|
||||
// Try falling back to normal fetch if using Civitai API fails
|
||||
whenever(civitaiErr, fetchFileSize, { once: true })
|
||||
} else {
|
||||
// Fetch file size in the background
|
||||
void fetchFileSize()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
triggerBrowserDownload,
|
||||
fileSize
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
||||
|
||||
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
import MissingModelsContent from '@/components/dialog/content/MissingModelsContent.vue'
|
||||
import MissingModelsFooter from '@/components/dialog/content/MissingModelsFooter.vue'
|
||||
import MissingModelsHeader from '@/components/dialog/content/MissingModelsHeader.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -14,11 +16,14 @@ export function useMissingModelsDialog() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show(props: ComponentAttrs<typeof MissingModelsWarning>) {
|
||||
function show(props: ComponentAttrs<typeof MissingModelsContent>) {
|
||||
showSmallLayoutDialog({
|
||||
key: DIALOG_KEY,
|
||||
component: MissingModelsWarning,
|
||||
props
|
||||
headerComponent: MissingModelsHeader,
|
||||
footerComponent: MissingModelsFooter,
|
||||
component: MissingModelsContent,
|
||||
props,
|
||||
footerProps: props
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user