mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
Queue media preview (#449)
* output url * Basic image previews * Split out task item component * Move task actions to context menu * simplify * Move spinner * Lift context menu to tab scope * Better tag * Fix placeholder style * nit * Correctly handle cancelled * nit * Split out result item as separate component * nit * Fix center crop * nit * Simplify task item * Flat list * Show prompt id * Make image draggable * Disable preview for dragging * Fix key * Correctly handle task in expanded view * Add preview
This commit is contained in:
@@ -1,125 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<DataTable
|
<SideBarTabTemplate :title="$t('sideToolbar.queue')">
|
||||||
v-if="tasks.length > 0"
|
<template #tool-buttons>
|
||||||
:value="tasks"
|
<Button
|
||||||
dataKey="promptId"
|
:icon="isExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
|
||||||
class="queue-table"
|
text
|
||||||
>
|
severity="secondary"
|
||||||
<Column header="STATUS">
|
@click="isExpanded = !isExpanded"
|
||||||
<template #body="{ data }">
|
class="toggle-expanded-button"
|
||||||
<Tag :severity="taskTagSeverity(data.displayStatus)">
|
v-tooltip="$t('sideToolbar.queueTab.showFlatList')"
|
||||||
{{ data.displayStatus.toUpperCase() }}
|
/>
|
||||||
</Tag>
|
<Button
|
||||||
</template>
|
icon="pi pi-trash"
|
||||||
</Column>
|
text
|
||||||
<Column header="TIME" :pt="{ root: { class: 'queue-time-cell' } }">
|
severity="primary"
|
||||||
<template #body="{ data }">
|
@click="confirmRemoveAll($event)"
|
||||||
<div v-if="data.isHistory" class="queue-time-cell-content">
|
class="clear-all-button"
|
||||||
{{ formatTime(data.executionTimeInSeconds) }}
|
/>
|
||||||
</div>
|
</template>
|
||||||
<div v-else-if="data.isRunning" class="queue-time-cell-content">
|
<template #body>
|
||||||
<i class="pi pi-spin pi-spinner"></i>
|
<div v-if="tasks.length > 0" class="queue-grid">
|
||||||
</div>
|
<TaskItem
|
||||||
<div v-else class="queue-time-cell-content">...</div>
|
v-for="task in tasks"
|
||||||
</template>
|
:key="task.key"
|
||||||
</Column>
|
:task="task"
|
||||||
<Column
|
:isFlatTask="isExpanded"
|
||||||
:pt="{
|
@contextmenu="handleContextMenu"
|
||||||
headerCell: {
|
|
||||||
class: 'queue-tool-header-cell'
|
|
||||||
},
|
|
||||||
bodyCell: {
|
|
||||||
class: 'queue-tool-body-cell'
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<Toast />
|
|
||||||
<ConfirmPopup />
|
|
||||||
<Button
|
|
||||||
icon="pi pi-trash"
|
|
||||||
text
|
|
||||||
severity="primary"
|
|
||||||
@click="confirmRemoveAll($event)"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</div>
|
||||||
<template #body="{ data }">
|
<div v-else>
|
||||||
<Button
|
<NoResultsPlaceholder
|
||||||
icon="pi pi-file-export"
|
icon="pi pi-info-circle"
|
||||||
text
|
:title="$t('noTasksFound')"
|
||||||
severity="primary"
|
:message="$t('noTasksFoundMessage')"
|
||||||
@click="data.loadWorkflow()"
|
|
||||||
/>
|
/>
|
||||||
<Button
|
</div>
|
||||||
icon="pi pi-times"
|
</template>
|
||||||
text
|
</SideBarTabTemplate>
|
||||||
severity="secondary"
|
<Toast />
|
||||||
@click="removeTask(data)"
|
<ConfirmPopup />
|
||||||
/>
|
<ContextMenu ref="menu" :model="menuItems" />
|
||||||
</template>
|
|
||||||
</Column>
|
|
||||||
</DataTable>
|
|
||||||
<div v-else>
|
|
||||||
<NoResultsPlaceholder
|
|
||||||
icon="pi pi-info-circle"
|
|
||||||
:title="$t('noTasksFound')"
|
|
||||||
:message="$t('noTasksFoundMessage')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DataTable from 'primevue/datatable'
|
|
||||||
import Column from 'primevue/column'
|
|
||||||
import Tag from 'primevue/tag'
|
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import ConfirmPopup from 'primevue/confirmpopup'
|
import ConfirmPopup from 'primevue/confirmpopup'
|
||||||
import Toast from 'primevue/toast'
|
import Toast from 'primevue/toast'
|
||||||
|
import ContextMenu from 'primevue/contextmenu'
|
||||||
|
import TaskItem from './queue/TaskItem.vue'
|
||||||
|
import SideBarTabTemplate from './SidebarTabTemplate.vue'
|
||||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||||
import { useConfirm } from 'primevue/useconfirm'
|
import { useConfirm } from 'primevue/useconfirm'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
import {
|
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
|
||||||
TaskItemDisplayStatus,
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
TaskItemImpl,
|
import { useI18n } from 'vue-i18n'
|
||||||
useQueueStore
|
import { type MenuItem } from 'primevue/menuitem'
|
||||||
} from '@/stores/queueStore'
|
|
||||||
import { computed, onMounted, onUnmounted } from 'vue'
|
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
|
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const tasks = computed(() =>
|
||||||
|
isExpanded.value ? queueStore.flatTasks : queueStore.tasks
|
||||||
|
)
|
||||||
|
|
||||||
const tasks = computed(() => queueStore.tasks)
|
|
||||||
const taskTagSeverity = (status: TaskItemDisplayStatus) => {
|
|
||||||
switch (status) {
|
|
||||||
case TaskItemDisplayStatus.Pending:
|
|
||||||
return 'secondary'
|
|
||||||
case TaskItemDisplayStatus.Running:
|
|
||||||
return 'info'
|
|
||||||
case TaskItemDisplayStatus.Completed:
|
|
||||||
return 'success'
|
|
||||||
case TaskItemDisplayStatus.Failed:
|
|
||||||
return 'danger'
|
|
||||||
case TaskItemDisplayStatus.Cancelled:
|
|
||||||
return 'warning'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const formatTime = (time?: number) => {
|
|
||||||
if (time === undefined) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return `${time.toFixed(2)}s`
|
|
||||||
}
|
|
||||||
const removeTask = (task: TaskItemImpl) => {
|
const removeTask = (task: TaskItemImpl) => {
|
||||||
if (task.isRunning) {
|
if (task.isRunning) {
|
||||||
api.interrupt()
|
api.interrupt()
|
||||||
}
|
}
|
||||||
queueStore.delete(task)
|
queueStore.delete(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeAllTasks = async () => {
|
const removeAllTasks = async () => {
|
||||||
await queueStore.clear()
|
await queueStore.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmRemoveAll = (event) => {
|
const confirmRemoveAll = (event) => {
|
||||||
confirm.require({
|
confirm.require({
|
||||||
target: event.currentTarget,
|
target: event.currentTarget,
|
||||||
@@ -147,30 +104,45 @@ const confirmRemoveAll = (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onStatus = () => queueStore.update()
|
const onStatus = () => queueStore.update()
|
||||||
|
|
||||||
|
const menu = ref(null)
|
||||||
|
const menuTargetTask = ref<TaskItemImpl | null>(null)
|
||||||
|
const menuItems = computed<MenuItem[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('delete'),
|
||||||
|
icon: 'pi pi-trash',
|
||||||
|
command: () => removeTask(menuTargetTask.value!)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('loadWorkflow'),
|
||||||
|
icon: 'pi pi-file-export',
|
||||||
|
command: () => menuTargetTask.value?.loadWorkflow()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const handleContextMenu = ({ task, event }) => {
|
||||||
|
menuTargetTask.value = task
|
||||||
|
menu.value.show(event)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
api.addEventListener('status', onStatus)
|
api.addEventListener('status', onStatus)
|
||||||
|
|
||||||
queueStore.update()
|
queueStore.update()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
api.removeEventListener('status', onStatus)
|
api.removeEventListener('status', onStatus)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isExpanded = ref(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.queue-tool-header-cell {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-tool-body-cell {
|
|
||||||
display: table-cell;
|
|
||||||
text-align: right !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.queue-time-cell-content {
|
.queue-grid {
|
||||||
width: fit-content;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const props = defineProps({
|
|||||||
border-top: none;
|
border-top: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 0.25rem 1rem;
|
padding: 0.25rem 1rem;
|
||||||
|
min-height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comfy-vue-side-bar-header-span {
|
.comfy-vue-side-bar-header-span {
|
||||||
|
|||||||
81
src/components/sidebar/tabs/queue/ResultItem.vue
Normal file
81
src/components/sidebar/tabs/queue/ResultItem.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="result-container" ref="resultContainer">
|
||||||
|
<Image
|
||||||
|
v-if="result.mediaType === 'images'"
|
||||||
|
:src="result.url"
|
||||||
|
alt="Task Output"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
preview
|
||||||
|
:pt="{ previewMask: { class: 'image-preview-mask' } }"
|
||||||
|
/>
|
||||||
|
<!-- TODO: handle more media types -->
|
||||||
|
<div v-else class="task-result-preview">
|
||||||
|
<i class="pi pi-file"></i>
|
||||||
|
<span>{{ result.mediaType }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ResultItemImpl } from '@/stores/queueStore'
|
||||||
|
import Image from 'primevue/image'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
result: ResultItemImpl
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const resultContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.result.mediaType === 'images') {
|
||||||
|
resultContainer.value.querySelectorAll('img').forEach((img) => {
|
||||||
|
img.draggable = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.result-container {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(img) {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-image-preview {
|
||||||
|
position: static;
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.image-preview-mask) {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: var(--p-image-preview-mask-color);
|
||||||
|
transition:
|
||||||
|
opacity var(--p-image-transition-duration),
|
||||||
|
background var(--p-image-transition-duration);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
144
src/components/sidebar/tabs/queue/TaskItem.vue
Normal file
144
src/components/sidebar/tabs/queue/TaskItem.vue
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<!-- @contextmenu="handleContextMenu" -->
|
||||||
|
<div class="task-item">
|
||||||
|
<div class="task-result-preview">
|
||||||
|
<div v-if="task.displayStatus === TaskItemDisplayStatus.Completed">
|
||||||
|
<ResultItem v-if="flatOutputs.length" :result="flatOutputs[0]" />
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
v-else-if="task.displayStatus === TaskItemDisplayStatus.Running"
|
||||||
|
class="pi pi-spin pi-spinner"
|
||||||
|
></i>
|
||||||
|
<span v-else-if="task.displayStatus === TaskItemDisplayStatus.Pending"
|
||||||
|
>...</span
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-else-if="task.displayStatus === TaskItemDisplayStatus.Cancelled"
|
||||||
|
class="pi pi-exclamation-triangle"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="task.displayStatus === TaskItemDisplayStatus.Failed"
|
||||||
|
class="pi pi-exclamation-circle"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-item-details">
|
||||||
|
<div class="tag-wrapper">
|
||||||
|
<Tag :severity="taskTagSeverity(task.displayStatus)">
|
||||||
|
<span v-html="taskStatusText(task.displayStatus)"></span>
|
||||||
|
<span v-if="task.isHistory" class="task-time">
|
||||||
|
{{ formatTime(task.executionTimeInSeconds) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="isFlatTask" class="task-prompt-id">
|
||||||
|
{{ task.promptId.split('-')[0] }}
|
||||||
|
</span>
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div class="tag-wrapper">
|
||||||
|
<Tag v-if="task.isHistory && flatOutputs.length > 1">
|
||||||
|
<span>{{ flatOutputs.length }}</span>
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Tag from 'primevue/tag'
|
||||||
|
import ResultItem from './ResultItem.vue'
|
||||||
|
import { TaskItemDisplayStatus, type TaskItemImpl } from '@/stores/queueStore'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
task: TaskItemImpl
|
||||||
|
isFlatTask: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const flatOutputs = props.task.flatOutputs
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'contextmenu', { task: TaskItemImpl, event: MouseEvent }): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleContextMenu = (e: MouseEvent) => {
|
||||||
|
emit('contextmenu', { task: props.task, event: e })
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskTagSeverity = (status: TaskItemDisplayStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case TaskItemDisplayStatus.Pending:
|
||||||
|
return 'secondary'
|
||||||
|
case TaskItemDisplayStatus.Running:
|
||||||
|
return 'info'
|
||||||
|
case TaskItemDisplayStatus.Completed:
|
||||||
|
return 'success'
|
||||||
|
case TaskItemDisplayStatus.Failed:
|
||||||
|
return 'danger'
|
||||||
|
case TaskItemDisplayStatus.Cancelled:
|
||||||
|
return 'warn'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskStatusText = (status: TaskItemDisplayStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case TaskItemDisplayStatus.Pending:
|
||||||
|
return 'Pending'
|
||||||
|
case TaskItemDisplayStatus.Running:
|
||||||
|
return 'Running'
|
||||||
|
case TaskItemDisplayStatus.Completed:
|
||||||
|
return '<i class="pi pi-check" style="font-weight: bold"></i>'
|
||||||
|
case TaskItemDisplayStatus.Failed:
|
||||||
|
return 'Failed'
|
||||||
|
case TaskItemDisplayStatus.Cancelled:
|
||||||
|
return 'Cancelled'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time?: number) => {
|
||||||
|
if (time === undefined) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return `${time.toFixed(2)}s`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.task-result-preview {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-result-preview i,
|
||||||
|
.task-result-preview span {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item-details {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 0.6rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* In dark mode, transparent background color for tags is not ideal for tags that
|
||||||
|
are floating on top of images. */
|
||||||
|
.tag-wrapper {
|
||||||
|
background-color: var(--p-primary-contrast-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
src/i18n.ts
10
src/i18n.ts
@@ -2,6 +2,8 @@ import { createI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
en: {
|
en: {
|
||||||
|
delete: 'Delete',
|
||||||
|
loadWorkflow: 'Load Workflow',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
searchSettings: 'Search Settings',
|
searchSettings: 'Search Settings',
|
||||||
noResultsFound: 'No Results Found',
|
noResultsFound: 'No Results Found',
|
||||||
@@ -15,10 +17,15 @@ const messages = {
|
|||||||
nodeLibrary: 'Node Library',
|
nodeLibrary: 'Node Library',
|
||||||
nodeLibraryTab: {
|
nodeLibraryTab: {
|
||||||
sortOrder: 'Sort Order'
|
sortOrder: 'Sort Order'
|
||||||
|
},
|
||||||
|
queueTab: {
|
||||||
|
showFlatList: 'Show Flat List'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
|
delete: '删除',
|
||||||
|
loadWorkflow: '加载工作流',
|
||||||
settings: '设置',
|
settings: '设置',
|
||||||
searchSettings: '搜索设置',
|
searchSettings: '搜索设置',
|
||||||
noResultsFound: '未找到结果',
|
noResultsFound: '未找到结果',
|
||||||
@@ -32,6 +39,9 @@ const messages = {
|
|||||||
nodeLibrary: '节点库',
|
nodeLibrary: '节点库',
|
||||||
nodeLibraryTab: {
|
nodeLibraryTab: {
|
||||||
sortOrder: '排序顺序'
|
sortOrder: '排序顺序'
|
||||||
|
},
|
||||||
|
queueTab: {
|
||||||
|
showFlatList: '平铺结果'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import {
|
import type {
|
||||||
validateTaskItem,
|
|
||||||
TaskItem,
|
TaskItem,
|
||||||
TaskType,
|
TaskType,
|
||||||
TaskPrompt,
|
TaskPrompt,
|
||||||
TaskStatus,
|
TaskStatus,
|
||||||
|
StatusWsMessageStatus,
|
||||||
TaskOutput,
|
TaskOutput,
|
||||||
StatusWsMessageStatus
|
ResultItem
|
||||||
} from '@/types/apiTypes'
|
} from '@/types/apiTypes'
|
||||||
import { plainToClass } from 'class-transformer'
|
import { validateTaskItem } from '@/types/apiTypes'
|
||||||
|
import type { NodeId } from '@/types/comfyWorkflow'
|
||||||
|
import { instanceToPlain, plainToClass } from 'class-transformer'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { toRaw } from 'vue'
|
import { toRaw } from 'vue'
|
||||||
@@ -25,11 +27,43 @@ export enum TaskItemDisplayStatus {
|
|||||||
Cancelled = 'Cancelled'
|
Cancelled = 'Cancelled'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ResultItemImpl {
|
||||||
|
filename: string
|
||||||
|
subfolder?: string
|
||||||
|
type: string
|
||||||
|
|
||||||
|
nodeId: NodeId
|
||||||
|
// 'audio' | 'images' | ...
|
||||||
|
mediaType: string
|
||||||
|
|
||||||
|
get url(): string {
|
||||||
|
return api.apiURL(`/view?filename=${encodeURIComponent(this.filename)}&type=${this.type}&
|
||||||
|
subfolder=${encodeURIComponent(this.subfolder || '')}&t=${+new Date()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class TaskItemImpl {
|
export class TaskItemImpl {
|
||||||
taskType: TaskType
|
taskType: TaskType
|
||||||
prompt: TaskPrompt
|
prompt: TaskPrompt
|
||||||
status?: TaskStatus
|
status?: TaskStatus
|
||||||
outputs?: TaskOutput
|
outputs: TaskOutput
|
||||||
|
|
||||||
|
get flatOutputs(): ResultItemImpl[] {
|
||||||
|
if (!this.outputs) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
|
||||||
|
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
||||||
|
(items as ResultItem[]).flatMap((item: ResultItem) =>
|
||||||
|
plainToClass(ResultItemImpl, {
|
||||||
|
...item,
|
||||||
|
nodeId,
|
||||||
|
mediaType
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
get apiTaskType(): APITaskType {
|
get apiTaskType(): APITaskType {
|
||||||
switch (this.taskType) {
|
switch (this.taskType) {
|
||||||
@@ -41,6 +75,10 @@ export class TaskItemImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get key() {
|
||||||
|
return this.promptId + this.displayStatus
|
||||||
|
}
|
||||||
|
|
||||||
get queueIndex() {
|
get queueIndex() {
|
||||||
return this.prompt[0]
|
return this.prompt[0]
|
||||||
}
|
}
|
||||||
@@ -174,6 +212,31 @@ export const useQueueStore = defineStore('queue', {
|
|||||||
...state.historyTasks
|
...state.historyTasks
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
flatTasks(): TaskItemImpl[] {
|
||||||
|
return this.tasks.flatMap((task: TaskItemImpl) => {
|
||||||
|
if (task.displayStatus !== TaskItemDisplayStatus.Completed) {
|
||||||
|
return [task]
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.flatOutputs.map((output: ResultItemImpl, i: number) =>
|
||||||
|
plainToClass(TaskItemImpl, {
|
||||||
|
...instanceToPlain(task),
|
||||||
|
prompt: [
|
||||||
|
task.queueIndex,
|
||||||
|
`${task.promptId}-${i}`,
|
||||||
|
task.promptInputs,
|
||||||
|
task.extraData,
|
||||||
|
task.outputsToExecute
|
||||||
|
],
|
||||||
|
outputs: {
|
||||||
|
[output.nodeId]: {
|
||||||
|
[output.mediaType]: [instanceToPlain(output)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
lastHistoryQueueIndex(state) {
|
lastHistoryQueueIndex(state) {
|
||||||
return state.historyTasks.length ? state.historyTasks[0].queueIndex : -1
|
return state.historyTasks.length ? state.historyTasks[0].queueIndex : -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,18 @@ import { colorPalettesSchema } from './colorPalette'
|
|||||||
const zNodeType = z.string()
|
const zNodeType = z.string()
|
||||||
const zQueueIndex = z.number()
|
const zQueueIndex = z.number()
|
||||||
const zPromptId = z.string()
|
const zPromptId = z.string()
|
||||||
const zImageResult = z.object({
|
const zResultItem = z.object({
|
||||||
filename: z.string(),
|
filename: z.string(),
|
||||||
subfolder: z.string().optional(),
|
subfolder: z.string().optional(),
|
||||||
type: z.string()
|
type: z.string()
|
||||||
})
|
})
|
||||||
|
export type ResultItem = z.infer<typeof zResultItem>
|
||||||
|
const zOutputs = z
|
||||||
|
.object({
|
||||||
|
audio: z.array(zResultItem).optional(),
|
||||||
|
images: z.array(zResultItem).optional()
|
||||||
|
})
|
||||||
|
.passthrough()
|
||||||
|
|
||||||
// WS messages
|
// WS messages
|
||||||
const zStatusWsMessageStatus = z.object({
|
const zStatusWsMessageStatus = z.object({
|
||||||
@@ -37,11 +44,7 @@ const zExecutingWsMessage = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const zExecutedWsMessage = zExecutingWsMessage.extend({
|
const zExecutedWsMessage = zExecutingWsMessage.extend({
|
||||||
outputs: z
|
outputs: zOutputs
|
||||||
.object({
|
|
||||||
images: z.array(zImageResult)
|
|
||||||
})
|
|
||||||
.passthrough()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const zExecutionWsMessageBase = z.object({
|
const zExecutionWsMessageBase = z.object({
|
||||||
@@ -144,9 +147,6 @@ const zStatus = z.object({
|
|||||||
messages: z.array(zStatusMessage)
|
messages: z.array(zStatusMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: this is a placeholder
|
|
||||||
const zOutput = z.any()
|
|
||||||
|
|
||||||
const zTaskPrompt = z.tuple([
|
const zTaskPrompt = z.tuple([
|
||||||
zQueueIndex,
|
zQueueIndex,
|
||||||
zPromptId,
|
zPromptId,
|
||||||
@@ -170,7 +170,7 @@ const zPendingTaskItem = z.object({
|
|||||||
prompt: zTaskPrompt
|
prompt: zTaskPrompt
|
||||||
})
|
})
|
||||||
|
|
||||||
const zTaskOutput = z.record(zNodeId, zOutput)
|
const zTaskOutput = z.record(zNodeId, zOutputs)
|
||||||
|
|
||||||
const zHistoryTaskItem = z.object({
|
const zHistoryTaskItem = z.object({
|
||||||
taskType: z.literal('History'),
|
taskType: z.literal('History'),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { fromZodError } from 'zod-validation-error'
|
|||||||
// innerNode.id = `${this.node.id}:${i}`
|
// innerNode.id = `${this.node.id}:${i}`
|
||||||
// Remove it after GroupNode is redesigned.
|
// Remove it after GroupNode is redesigned.
|
||||||
export const zNodeId = z.union([z.number().int(), z.string()])
|
export const zNodeId = z.union([z.number().int(), z.string()])
|
||||||
|
export type NodeId = z.infer<typeof zNodeId>
|
||||||
export const zSlotIndex = z.union([
|
export const zSlotIndex = z.union([
|
||||||
z.number().int(),
|
z.number().int(),
|
||||||
z
|
z
|
||||||
|
|||||||
Reference in New Issue
Block a user