mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Filter cached/canceled results (#1586)
* Filter cached/canceled results * Highlight if on * Update setting
This commit is contained in:
@@ -1,6 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<SidebarTabTemplate :title="$t('sideToolbar.queue')">
|
<SidebarTabTemplate :title="$t('sideToolbar.queue')">
|
||||||
<template #tool-buttons>
|
<template #tool-buttons>
|
||||||
|
<Popover ref="outputFilterPopup">
|
||||||
|
<OutputFilters />
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon="pi pi-filter"
|
||||||
|
text
|
||||||
|
severity="secondary"
|
||||||
|
@click="outputFilterPopup.toggle($event)"
|
||||||
|
v-tooltip="$t(`sideToolbar.queueTab.filter`)"
|
||||||
|
:class="{ 'text-yellow-500': anyFilter }"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
:icon="
|
:icon="
|
||||||
imageFit === 'cover'
|
imageFit === 'cover'
|
||||||
@@ -99,6 +111,7 @@ import Button from 'primevue/button'
|
|||||||
import ConfirmPopup from 'primevue/confirmpopup'
|
import ConfirmPopup from 'primevue/confirmpopup'
|
||||||
import ContextMenu from 'primevue/contextmenu'
|
import ContextMenu from 'primevue/contextmenu'
|
||||||
import type { MenuItem } from 'primevue/menuitem'
|
import type { MenuItem } from 'primevue/menuitem'
|
||||||
|
import Popover from 'primevue/popover'
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
import ProgressSpinner from 'primevue/progressspinner'
|
||||||
import TaskItem from './queue/TaskItem.vue'
|
import TaskItem from './queue/TaskItem.vue'
|
||||||
import ResultGallery from './queue/ResultGallery.vue'
|
import ResultGallery from './queue/ResultGallery.vue'
|
||||||
@@ -111,7 +124,9 @@ import { useSettingStore } from '@/stores/settingStore'
|
|||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
|
|
||||||
const IMAGE_FIT = 'Comfy.Queue.ImageFit'
|
const SETTING_FIT = 'Comfy.Queue.ImageFit'
|
||||||
|
const SETTING_FLAT = 'Comfy.Queue.ShowFlatList'
|
||||||
|
const SETTING_FILTER = 'Comfy.Queue.Filter'
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
@@ -120,7 +135,7 @@ const commandStore = useCommandStore()
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// Expanded view: show all outputs in a flat list.
|
// Expanded view: show all outputs in a flat list.
|
||||||
const isExpanded = ref(false)
|
const isExpanded = computed<boolean>(() => settingStore.get(SETTING_FLAT))
|
||||||
const visibleTasks = ref<TaskItemImpl[]>([])
|
const visibleTasks = ref<TaskItemImpl[]>([])
|
||||||
const scrollContainer = ref<HTMLElement | null>(null)
|
const scrollContainer = ref<HTMLElement | null>(null)
|
||||||
const loadMoreTrigger = ref<HTMLElement | null>(null)
|
const loadMoreTrigger = ref<HTMLElement | null>(null)
|
||||||
@@ -128,7 +143,23 @@ const galleryActiveIndex = ref(-1)
|
|||||||
// Folder view: only show outputs from a single selected task.
|
// Folder view: only show outputs from a single selected task.
|
||||||
const folderTask = ref<TaskItemImpl | null>(null)
|
const folderTask = ref<TaskItemImpl | null>(null)
|
||||||
const isInFolderView = computed(() => folderTask.value !== null)
|
const isInFolderView = computed(() => folderTask.value !== null)
|
||||||
const imageFit = computed<string>(() => settingStore.get(IMAGE_FIT))
|
const imageFit = computed<string>(() => settingStore.get(SETTING_FIT))
|
||||||
|
const hideCached = computed<boolean>(
|
||||||
|
() => settingStore.get(SETTING_FILTER)?.hideCached
|
||||||
|
)
|
||||||
|
const hideCanceled = computed<boolean>(
|
||||||
|
() => settingStore.get(SETTING_FILTER)?.hideCanceled
|
||||||
|
)
|
||||||
|
const anyFilter = computed(() => hideCanceled.value || hideCached.value)
|
||||||
|
|
||||||
|
watch(hideCached, () => {
|
||||||
|
updateVisibleTasks()
|
||||||
|
})
|
||||||
|
watch(hideCanceled, () => {
|
||||||
|
updateVisibleTasks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const outputFilterPopup = ref(null)
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 8
|
const ITEMS_PER_PAGE = 8
|
||||||
const SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load
|
const SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load
|
||||||
@@ -149,9 +180,31 @@ const allGalleryItems = computed(() =>
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const filterTasks = (tasks: TaskItemImpl[]) =>
|
||||||
|
tasks
|
||||||
|
.filter((t) => {
|
||||||
|
if (
|
||||||
|
hideCanceled.value &&
|
||||||
|
t.status?.messages?.at(-1)?.[0] === 'execution_interrupted'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hideCached.value &&
|
||||||
|
t.flatOutputs?.length &&
|
||||||
|
t.flatOutputs.every((o) => o.cached)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.slice(0, ITEMS_PER_PAGE)
|
||||||
|
|
||||||
const loadMoreItems = () => {
|
const loadMoreItems = () => {
|
||||||
const currentLength = visibleTasks.value.length
|
const currentLength = visibleTasks.value.length
|
||||||
const newTasks = allTasks.value.slice(
|
const newTasks = filterTasks(allTasks.value).slice(
|
||||||
currentLength,
|
currentLength,
|
||||||
currentLength + ITEMS_PER_PAGE
|
currentLength + ITEMS_PER_PAGE
|
||||||
)
|
)
|
||||||
@@ -186,11 +239,11 @@ useResizeObserver(scrollContainer, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const updateVisibleTasks = () => {
|
const updateVisibleTasks = () => {
|
||||||
visibleTasks.value = allTasks.value.slice(0, ITEMS_PER_PAGE)
|
visibleTasks.value = filterTasks(allTasks.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleExpanded = () => {
|
const toggleExpanded = () => {
|
||||||
isExpanded.value = !isExpanded.value
|
settingStore.set(SETTING_FLAT, !isExpanded.value)
|
||||||
updateVisibleTasks()
|
updateVisibleTasks()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +344,10 @@ const exitFolderView = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleImageFit = () => {
|
const toggleImageFit = () => {
|
||||||
settingStore.set(IMAGE_FIT, imageFit.value === 'cover' ? 'contain' : 'cover')
|
settingStore.set(
|
||||||
|
SETTING_FIT,
|
||||||
|
imageFit.value === 'cover' ? 'contain' : 'cover'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
38
src/components/sidebar/tabs/queue/OutputFilters.vue
Normal file
38
src/components/sidebar/tabs/queue/OutputFilters.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
{{ $t('sideToolbar.queueTab.filters.hideCached') }}
|
||||||
|
<ToggleSwitch v-model="hideCached" />
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
{{ $t('sideToolbar.queueTab.filters.hideCanceled') }}
|
||||||
|
<ToggleSwitch v-model="hideCanceled" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ToggleSwitch from 'primevue/toggleswitch'
|
||||||
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
const SETTING_FILTER = 'Comfy.Queue.Filter'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const filter = settingStore.get(SETTING_FILTER) ?? {}
|
||||||
|
|
||||||
|
const createCompute = (k: string) =>
|
||||||
|
computed({
|
||||||
|
get() {
|
||||||
|
return filter[k]
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
filter[k] = value
|
||||||
|
settingStore.set(SETTING_FILTER, filter)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const hideCached = createCompute('hideCached')
|
||||||
|
const hideCanceled = createCompute('hideCanceled')
|
||||||
|
</script>
|
||||||
@@ -31,16 +31,20 @@
|
|||||||
|
|
||||||
<div class="task-item-details">
|
<div class="task-item-details">
|
||||||
<div class="tag-wrapper status-tag-group">
|
<div class="tag-wrapper status-tag-group">
|
||||||
<Tag v-if="isFlatTask && task.isHistory" class="node-name-tag">
|
<Tag v-if="isFlatTask && task.isHistory && node" class="node-name-tag">
|
||||||
<Button
|
<Button
|
||||||
class="task-node-link"
|
class="task-node-link"
|
||||||
:label="`${node?.type} (#${node?.id})`"
|
:label="`${node.type} (#${node.id})`"
|
||||||
link
|
link
|
||||||
size="small"
|
size="small"
|
||||||
@click="app.goToNode(node?.id)"
|
@click="app.goToNode(node?.id)"
|
||||||
/>
|
/>
|
||||||
</Tag>
|
</Tag>
|
||||||
<Tag :severity="taskTagSeverity(task.displayStatus)">
|
<Tag
|
||||||
|
:severity="taskTagSeverity(task.displayStatus)"
|
||||||
|
class="task-duration relative"
|
||||||
|
>
|
||||||
|
<i v-if="isCachedResult" class="pi pi-server task-cached-icon"></i>
|
||||||
<span v-html="taskStatusText(task.displayStatus)"></span>
|
<span v-html="taskStatusText(task.displayStatus)"></span>
|
||||||
<span v-if="task.isHistory" class="task-time">
|
<span v-if="task.isHistory" class="task-time">
|
||||||
{{ formatTime(task.executionTimeInSeconds) }}
|
{{ formatTime(task.executionTimeInSeconds) }}
|
||||||
@@ -90,6 +94,7 @@ const node: ComfyNode | null =
|
|||||||
) ?? null
|
) ?? null
|
||||||
: null
|
: null
|
||||||
const progressPreviewBlobUrl = ref('')
|
const progressPreviewBlobUrl = ref('')
|
||||||
|
const isCachedResult = props.isFlatTask && coverResult?.cached
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(
|
(
|
||||||
@@ -142,7 +147,7 @@ const taskStatusText = (status: TaskItemDisplayStatus) => {
|
|||||||
case TaskItemDisplayStatus.Running:
|
case TaskItemDisplayStatus.Running:
|
||||||
return '<i class="pi pi-spin pi-spinner" style="font-weight: bold"></i> Running'
|
return '<i class="pi pi-spin pi-spinner" style="font-weight: bold"></i> Running'
|
||||||
case TaskItemDisplayStatus.Completed:
|
case TaskItemDisplayStatus.Completed:
|
||||||
return '<i class="pi pi-check" style="font-weight: bold"></i>'
|
return `<i class="pi pi-check${isCachedResult ? ' cached' : ''}" style="font-weight: bold"></i>`
|
||||||
case TaskItemDisplayStatus.Failed:
|
case TaskItemDisplayStatus.Failed:
|
||||||
return 'Failed'
|
return 'Failed'
|
||||||
case TaskItemDisplayStatus.Cancelled:
|
case TaskItemDisplayStatus.Cancelled:
|
||||||
@@ -226,4 +231,15 @@ are floating on top of images. */
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-cached-icon {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.pi-check.cached) {
|
||||||
|
font-size: 12px;
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -126,7 +126,12 @@ const messages = {
|
|||||||
backToAllTasks: 'Back to All Tasks',
|
backToAllTasks: 'Back to All Tasks',
|
||||||
containImagePreview: 'Fill Image Preview',
|
containImagePreview: 'Fill Image Preview',
|
||||||
coverImagePreview: 'Fit Image Preview',
|
coverImagePreview: 'Fit Image Preview',
|
||||||
clearPendingTasks: 'Clear Pending Tasks'
|
clearPendingTasks: 'Clear Pending Tasks',
|
||||||
|
filter: 'Filter Outputs',
|
||||||
|
filters: {
|
||||||
|
hideCached: 'Hide Cached',
|
||||||
|
hideCanceled: 'Hide Canceled'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
menu: {
|
menu: {
|
||||||
|
|||||||
@@ -198,6 +198,25 @@ export const CORE_SETTINGS: SettingParams[] = [
|
|||||||
type: 'hidden',
|
type: 'hidden',
|
||||||
defaultValue: 'cover'
|
defaultValue: 'cover'
|
||||||
},
|
},
|
||||||
|
// Hidden setting used by the queue for if the results should be grouped by prompt
|
||||||
|
{
|
||||||
|
id: 'Comfy.Queue.ShowFlatList',
|
||||||
|
name: 'Queue show flat list',
|
||||||
|
type: 'hidden',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hidden setting used by the queue to filter certain results
|
||||||
|
{
|
||||||
|
id: 'Comfy.Queue.Filter',
|
||||||
|
name: 'Queue output filters',
|
||||||
|
type: 'hidden',
|
||||||
|
defaultValue: {
|
||||||
|
hideCanceled: false,
|
||||||
|
hideCached: false
|
||||||
|
},
|
||||||
|
versionAdded: '1.4.3'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'Comfy.GroupSelectedNodes.Padding',
|
id: 'Comfy.GroupSelectedNodes.Padding',
|
||||||
category: ['LiteGraph', 'Group', 'Padding'],
|
category: ['LiteGraph', 'Group', 'Padding'],
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class ResultItemImpl {
|
|||||||
filename: string
|
filename: string
|
||||||
subfolder: string
|
subfolder: string
|
||||||
type: string
|
type: string
|
||||||
|
cached: boolean
|
||||||
|
|
||||||
nodeId: NodeId
|
nodeId: NodeId
|
||||||
// 'audio' | 'images' | ...
|
// 'audio' | 'images' | ...
|
||||||
@@ -43,6 +44,7 @@ export class ResultItemImpl {
|
|||||||
this.filename = obj.filename ?? ''
|
this.filename = obj.filename ?? ''
|
||||||
this.subfolder = obj.subfolder ?? ''
|
this.subfolder = obj.subfolder ?? ''
|
||||||
this.type = obj.type ?? ''
|
this.type = obj.type ?? ''
|
||||||
|
this.cached = obj.cached
|
||||||
|
|
||||||
this.nodeId = obj.nodeId
|
this.nodeId = obj.nodeId
|
||||||
this.mediaType = obj.mediaType
|
this.mediaType = obj.mediaType
|
||||||
@@ -149,6 +151,13 @@ export class TaskItemImpl {
|
|||||||
if (!this.outputs) {
|
if (!this.outputs) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedEntries = new Set(
|
||||||
|
this.status?.messages?.find((message) => {
|
||||||
|
return message[0] === 'execution_cached'
|
||||||
|
})?.[1].nodes
|
||||||
|
)
|
||||||
|
|
||||||
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
|
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
|
||||||
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
||||||
(items as ResultItem[]).map(
|
(items as ResultItem[]).map(
|
||||||
@@ -156,7 +165,8 @@ export class TaskItemImpl {
|
|||||||
new ResultItemImpl({
|
new ResultItemImpl({
|
||||||
...item,
|
...item,
|
||||||
nodeId,
|
nodeId,
|
||||||
mediaType
|
mediaType,
|
||||||
|
cached: cachedEntries.has(nodeId)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -454,6 +454,11 @@ const zNodeBadgeMode = z.enum(
|
|||||||
Object.values(NodeBadgeMode) as [string, ...string[]]
|
Object.values(NodeBadgeMode) as [string, ...string[]]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const zQueueFilter = z.object({
|
||||||
|
hideCached: z.boolean(),
|
||||||
|
hideCanceled: z.boolean()
|
||||||
|
})
|
||||||
|
|
||||||
const zSettings = z.record(z.any()).and(
|
const zSettings = z.record(z.any()).and(
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
@@ -506,6 +511,8 @@ const zSettings = z.record(z.any()).and(
|
|||||||
'Comfy.Validation.Workflows': z.boolean(),
|
'Comfy.Validation.Workflows': z.boolean(),
|
||||||
'Comfy.Workflow.SortNodeIdOnSave': z.boolean(),
|
'Comfy.Workflow.SortNodeIdOnSave': z.boolean(),
|
||||||
'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']),
|
'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']),
|
||||||
|
'Comfy.Queue.ShowFlatList': z.boolean(),
|
||||||
|
'Comfy.Queue.Filter': zQueueFilter.passthrough(),
|
||||||
'Comfy.Workflow.WorkflowTabsPosition': z.enum(['Sidebar', 'Topbar']),
|
'Comfy.Workflow.WorkflowTabsPosition': z.enum(['Sidebar', 'Topbar']),
|
||||||
'Comfy.Node.DoubleClickTitleToEdit': z.boolean(),
|
'Comfy.Node.DoubleClickTitleToEdit': z.boolean(),
|
||||||
'Comfy.Window.UnloadConfirmation': z.boolean(),
|
'Comfy.Window.UnloadConfirmation': z.boolean(),
|
||||||
|
|||||||
Reference in New Issue
Block a user