Skip to content

Commit 8dddffe

Browse files
authored
Use browser download in missing model dialog (#1362)
* Remove custom backend download logic * Add download hooks * Download button * Use browser download * Update test
1 parent 1f91a88 commit 8dddffe

File tree

10 files changed

+118
-293
lines changed

10 files changed

+118
-293
lines changed

browser_tests/dialog.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,11 @@ test.describe('Missing models warning', () => {
6363

6464
const downloadButton = comfyPage.page.getByLabel('Download')
6565
await expect(downloadButton).toBeVisible()
66+
const downloadPromise = comfyPage.page.waitForEvent('download')
6667
await downloadButton.click()
6768

68-
const downloadComplete = comfyPage.page.locator('.download-complete')
69-
await expect(downloadComplete).toBeVisible()
69+
const download = await downloadPromise
70+
expect(download.suggestedFilename()).toBe('fake_model.safetensors')
7071
})
7172
})
7273

src/components/common/DeviceInfo.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
<script setup lang="ts">
1111
import type { DeviceStats } from '@/types/apiTypes'
12-
import { formatMemory } from '@/utils/formatUtil'
12+
import { formatSize } from '@/utils/formatUtil'
1313
1414
const props = defineProps<{
1515
device: DeviceStats
@@ -30,7 +30,7 @@ const formatValue = (value: any, field: string) => {
3030
field
3131
)
3232
) {
33-
return formatMemory(value)
33+
return formatSize(value)
3434
}
3535
return value
3636
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!-- A file download button with a label and a size hint -->
2+
<template>
3+
<div class="flex flex-row items-center gap-2">
4+
<div class="file-info">
5+
<div class="file-details">
6+
<span class="file-type" :title="hint">{{ label }}</span>
7+
</div>
8+
<div v-if="props.error" class="file-error">
9+
{{ props.error }}
10+
</div>
11+
</div>
12+
<div class="file-action">
13+
<Button
14+
class="file-action-button"
15+
:label="$t('download') + ' (' + fileSize + ')'"
16+
size="small"
17+
outlined
18+
:disabled="props.error"
19+
@click="download.triggerBrowserDownload"
20+
/>
21+
</div>
22+
</div>
23+
</template>
24+
25+
<script setup lang="ts">
26+
import { useDownload } from '@/hooks/downloadHooks'
27+
import Button from 'primevue/button'
28+
import { computed } from 'vue'
29+
import { formatSize } from '@/utils/formatUtil'
30+
31+
const props = defineProps<{
32+
url: string
33+
hint?: string
34+
label?: string
35+
error?: string
36+
}>()
37+
38+
const label = computed(() => props.label || props.url.split('/').pop())
39+
const hint = computed(() => props.hint || props.url)
40+
const download = useDownload(props.url)
41+
const fileSize = computed(() =>
42+
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
43+
)
44+
</script>

src/components/common/SystemStatsPanel.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import TabPanel from 'primevue/tabpanel'
3535
import Divider from 'primevue/divider'
3636
import type { SystemStats } from '@/types/apiTypes'
3737
import DeviceInfo from '@/components/common/DeviceInfo.vue'
38-
import { formatMemory } from '@/utils/formatUtil'
38+
import { formatSize } from '@/utils/formatUtil'
3939
4040
const props = defineProps<{
4141
stats: SystemStats
@@ -58,7 +58,7 @@ const systemColumns = [
5858
5959
const formatValue = (value: any, field: string) => {
6060
if (['ram_total', 'ram_free'].includes(field)) {
61-
return formatMemory(value)
61+
return formatSize(value)
6262
}
6363
return value
6464
}

src/components/dialog/content/MissingModelsWarning.vue

Lines changed: 13 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -5,80 +5,22 @@
55
title="Missing Models"
66
message="When loading the graph, the following models were not found"
77
/>
8-
<ListBox
9-
:options="missingModels"
10-
optionLabel="label"
11-
scrollHeight="100%"
12-
class="comfy-missing-models"
13-
>
14-
<template #option="slotProps">
15-
<div
16-
class="missing-model-item flex flex-row items-center"
17-
:style="{ '--progress': `${slotProps.option.progress}%` }"
18-
>
19-
<div class="model-info">
20-
<div class="model-details">
21-
<span class="model-type" :title="slotProps.option.hint">{{
22-
slotProps.option.label
23-
}}</span>
24-
</div>
25-
<div v-if="slotProps.option.error" class="model-error">
26-
{{ slotProps.option.error }}
27-
</div>
28-
</div>
29-
<div class="model-action">
30-
<Select
31-
class="model-path-select mr-2"
32-
v-if="
33-
slotProps.option.action &&
34-
!slotProps.option.downloading &&
35-
!slotProps.option.completed &&
36-
!slotProps.option.error
37-
"
38-
v-show="slotProps.option.paths.length > 1"
39-
v-model="slotProps.option.folderPath"
40-
:options="slotProps.option.paths"
41-
@change="updateFolderPath(slotProps.option, $event)"
42-
/>
43-
<Button
44-
v-if="
45-
slotProps.option.action &&
46-
!slotProps.option.downloading &&
47-
!slotProps.option.completed &&
48-
!slotProps.option.error
49-
"
50-
@click="slotProps.option.action.callback"
51-
:label="slotProps.option.action.text"
52-
size="small"
53-
outlined
54-
class="model-action-button"
55-
/>
56-
<div v-if="slotProps.option.downloading" class="download-progress">
57-
<span class="progress-text"
58-
>{{ slotProps.option.progress.toFixed(2) }}%</span
59-
>
60-
</div>
61-
<div v-if="slotProps.option.completed" class="download-complete">
62-
<i class="pi pi-check" style="color: var(--p-green-500)"></i>
63-
</div>
64-
<div v-if="slotProps.option.error" class="download-error">
65-
<i class="pi pi-times" style="color: var(--p-red-600)"></i>
66-
</div>
67-
</div>
68-
</div>
8+
<ListBox :options="missingModels" class="comfy-missing-models">
9+
<template #option="{ option }">
10+
<FileDownload
11+
:url="option.url"
12+
:label="option.label"
13+
:error="option.error"
14+
/>
6915
</template>
7016
</ListBox>
7117
</template>
7218

7319
<script setup lang="ts">
7420
import { ref, computed } from 'vue'
7521
import ListBox from 'primevue/listbox'
76-
import Select from 'primevue/select'
7722
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
78-
import { SelectChangeEvent } from 'primevue/select'
79-
import Button from 'primevue/button'
80-
import { api } from '@/scripts/api'
81-
import { DownloadModelStatus } from '@/types/apiTypes'
23+
import FileDownload from '@/components/common/FileDownload.vue'
8224
8325
// TODO: Read this from server internal API rather than hardcoding here
8426
// as some installations may wish to use custom sources
@@ -107,86 +49,13 @@ const props = defineProps<{
10749
}>()
10850
10951
const modelDownloads = ref<Record<string, ModelInfo>>({})
110-
let lastModel: string | null = null
111-
112-
const updateFolderPath = (model: any, event: SelectChangeEvent) => {
113-
const downloadInfo = modelDownloads.value[model.name]
114-
downloadInfo.folder_path = event.value
115-
return false
116-
}
117-
const handleDownloadProgress = (detail: DownloadModelStatus) => {
118-
if (detail.download_path) {
119-
lastModel = detail.download_path
120-
}
121-
if (!lastModel) return
122-
if (detail.status === 'in_progress') {
123-
modelDownloads.value[lastModel] = {
124-
...modelDownloads.value[lastModel],
125-
downloading: true,
126-
progress: detail.progress_percentage,
127-
completed: false
128-
}
129-
} else if (detail.status === 'pending') {
130-
modelDownloads.value[lastModel] = {
131-
...modelDownloads.value[lastModel],
132-
downloading: true,
133-
progress: 0,
134-
completed: false
135-
}
136-
} else if (detail.status === 'completed') {
137-
modelDownloads.value[lastModel] = {
138-
...modelDownloads.value[lastModel],
139-
downloading: false,
140-
progress: 100,
141-
completed: true
142-
}
143-
} else if (detail.status === 'error') {
144-
modelDownloads.value[lastModel] = {
145-
...modelDownloads.value[lastModel],
146-
downloading: false,
147-
progress: 0,
148-
error: detail.message,
149-
completed: false
150-
}
151-
}
152-
// TODO: other statuses?
153-
}
154-
155-
const triggerDownload = async (
156-
url: string,
157-
directory: string,
158-
filename: string,
159-
folder_path: string
160-
) => {
161-
modelDownloads.value[filename] = {
162-
name: filename,
163-
directory,
164-
url,
165-
downloading: true,
166-
progress: 0
167-
}
168-
const download = await api.internalDownloadModel(
169-
url,
170-
directory,
171-
filename,
172-
1,
173-
folder_path
174-
)
175-
lastModel = filename
176-
handleDownloadProgress(download)
177-
}
178-
179-
api.addEventListener('download_progress', (event: CustomEvent) => {
180-
handleDownloadProgress(event.detail)
181-
})
182-
18352
const missingModels = computed(() => {
18453
return props.missingModels.map((model) => {
18554
const paths = props.paths[model.directory]
18655
if (model.directory_invalid || !paths) {
18756
return {
18857
label: `${model.directory} / ${model.name}`,
189-
hint: model.url,
58+
url: model.url,
19059
error: 'Invalid directory specified (does this require custom nodes?)'
19160
}
19261
}
@@ -204,37 +73,27 @@ const missingModels = computed(() => {
20473
if (!allowedSources.some((source) => model.url.startsWith(source))) {
20574
return {
20675
label: `${model.directory} / ${model.name}`,
207-
hint: model.url,
76+
url: model.url,
20877
error: `Download not allowed from source '${model.url}', only allowed from '${allowedSources.join("', '")}'`
20978
}
21079
}
21180
if (!allowedSuffixes.some((suffix) => model.name.endsWith(suffix))) {
21281
return {
21382
label: `${model.directory} / ${model.name}`,
214-
hint: model.url,
83+
url: model.url,
21584
error: `Only allowed suffixes are: '${allowedSuffixes.join("', '")}'`
21685
}
21786
}
21887
return {
88+
url: model.url,
21989
label: `${model.directory} / ${model.name}`,
220-
hint: model.url,
22190
downloading: downloadInfo.downloading,
22291
completed: downloadInfo.completed,
22392
progress: downloadInfo.progress,
22493
error: downloadInfo.error,
22594
name: model.name,
22695
paths: paths,
227-
folderPath: downloadInfo.folder_path,
228-
action: {
229-
text: 'Download',
230-
callback: () =>
231-
triggerDownload(
232-
model.url,
233-
model.directory,
234-
model.name,
235-
downloadInfo.folder_path
236-
)
237-
}
96+
folderPath: downloadInfo.folder_path
23897
}
23998
})
24099
})
@@ -245,85 +104,4 @@ const missingModels = computed(() => {
245104
max-height: 300px;
246105
overflow-y: auto;
247106
}
248-
249-
.missing-model-item::before {
250-
content: '';
251-
position: absolute;
252-
top: 0;
253-
left: 0;
254-
height: 100%;
255-
width: var(--progress);
256-
background-color: var(--p-green-500);
257-
opacity: 0.2;
258-
transition: width 0.3s ease;
259-
}
260-
261-
.model-info {
262-
flex: 1;
263-
min-width: 0;
264-
z-index: 1;
265-
display: flex;
266-
flex-direction: column;
267-
margin-right: 1rem;
268-
overflow: hidden;
269-
}
270-
271-
.model-details {
272-
display: flex;
273-
align-items: center;
274-
flex-wrap: wrap;
275-
}
276-
277-
.model-type {
278-
font-weight: 600;
279-
color: var(--text-color);
280-
margin-right: 0.5rem;
281-
white-space: nowrap;
282-
overflow: hidden;
283-
text-overflow: ellipsis;
284-
}
285-
286-
.model-hint {
287-
font-style: italic;
288-
color: var(--text-color-secondary);
289-
white-space: nowrap;
290-
overflow: hidden;
291-
text-overflow: ellipsis;
292-
}
293-
294-
.model-error {
295-
color: var(--p-red-600);
296-
font-size: 0.8rem;
297-
margin-top: 0.25rem;
298-
}
299-
300-
.model-action {
301-
display: flex;
302-
align-items: center;
303-
justify-content: flex-end;
304-
z-index: 1;
305-
}
306-
307-
.model-action-button {
308-
min-width: 80px;
309-
}
310-
311-
.download-progress,
312-
.download-complete,
313-
.download-error {
314-
display: flex;
315-
align-items: center;
316-
justify-content: center;
317-
min-width: 80px;
318-
}
319-
320-
.progress-text {
321-
font-size: 0.8rem;
322-
color: var(--text-color);
323-
}
324-
325-
.download-complete i,
326-
.download-error i {
327-
font-size: 1.2rem;
328-
}
329107
</style>

0 commit comments

Comments
 (0)