mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-19 03:47:31 +00:00
Compare commits
10 Commits
implement-
...
js/drafts/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fcde239de | ||
|
|
1eba468f55 | ||
|
|
48ac4a2b36 | ||
|
|
c9c1275e4c | ||
|
|
78ebc54ebe | ||
|
|
88f2cc7847 | ||
|
|
7907e206da | ||
|
|
c4fa3dfe5a | ||
|
|
587d7a19a1 | ||
|
|
9ca705381c |
@@ -232,7 +232,11 @@ watch(
|
||||
|
||||
if (!isEmptySearch.value) {
|
||||
displayPacks.value = filterOutdatedPacks(installedPacks.value)
|
||||
} else if (!installedPacks.value.length) {
|
||||
} else if (
|
||||
!installedPacks.value.length &&
|
||||
!installedPacksReady.value &&
|
||||
!isLoadingInstalled.value
|
||||
) {
|
||||
await startFetchInstalled()
|
||||
} else {
|
||||
displayPacks.value = filterOutdatedPacks(installedPacks.value)
|
||||
|
||||
@@ -51,6 +51,7 @@ const isLoading = ref(false)
|
||||
const registryNodeDefs = shallowRef<ListComfyNodesResponse | null>(null)
|
||||
|
||||
const fetchNodeDefs = async () => {
|
||||
getNodeDefs.cancel()
|
||||
isLoading.value = true
|
||||
|
||||
const { id: packId } = nodePack
|
||||
|
||||
@@ -1,11 +1,37 @@
|
||||
<template>
|
||||
<img
|
||||
:src="isImageError ? DEFAULT_BANNER : imgSrc"
|
||||
:alt="nodePack.name + ' banner'"
|
||||
class="object-cover"
|
||||
:style="{ width: cssWidth, height: cssHeight }"
|
||||
@error="isImageError = true"
|
||||
/>
|
||||
<div :style="{ width: cssWidth, height: cssHeight }" class="overflow-hidden">
|
||||
<!-- default banner show -->
|
||||
<div v-if="showDefaultBanner" class="w-full h-full">
|
||||
<img
|
||||
:src="DEFAULT_BANNER"
|
||||
alt="default banner"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<!-- banner_url or icon show -->
|
||||
<div v-else class="relative w-full h-full">
|
||||
<!-- blur background -->
|
||||
<div
|
||||
v-if="imgSrc"
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-30"
|
||||
:style="{
|
||||
backgroundImage: `url(${imgSrc})`,
|
||||
filter: 'blur(10px)'
|
||||
}"
|
||||
></div>
|
||||
<!-- image -->
|
||||
<img
|
||||
:src="isImageError ? DEFAULT_BANNER : imgSrc"
|
||||
:alt="nodePack.name + ' banner'"
|
||||
:class="
|
||||
isImageError
|
||||
? 'relative w-full h-full object-cover z-10'
|
||||
: 'relative w-full h-full object-contain z-10'
|
||||
"
|
||||
@error="isImageError = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -20,18 +46,15 @@ const {
|
||||
width = '100%',
|
||||
height = '12rem'
|
||||
} = defineProps<{
|
||||
nodePack: components['schemas']['Node'] & { banner?: string } // Temporary measure until banner is in backend
|
||||
nodePack: components['schemas']['Node']
|
||||
width?: string
|
||||
height?: string
|
||||
}>()
|
||||
|
||||
const isImageError = ref(false)
|
||||
const shouldShowFallback = computed(
|
||||
() => !nodePack.banner || nodePack.banner.trim() === '' || isImageError.value
|
||||
)
|
||||
const imgSrc = computed(() =>
|
||||
shouldShowFallback.value ? DEFAULT_BANNER : nodePack.banner
|
||||
)
|
||||
|
||||
const showDefaultBanner = computed(() => !nodePack.banner_url && !nodePack.icon)
|
||||
const imgSrc = computed(() => nodePack.banner_url || nodePack.icon)
|
||||
|
||||
const convertToCssValue = (value: string | number) =>
|
||||
typeof value === 'number' ? `${value}rem` : value
|
||||
|
||||
@@ -113,11 +113,15 @@ import PackBanner from '@/components/dialog/content/manager/packBanner/PackBanne
|
||||
import PackCardFooter from '@/components/dialog/content/manager/packCard/PackCardFooter.vue'
|
||||
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import {
|
||||
IsInstallingKey,
|
||||
type MergedNodePack,
|
||||
type RegistryPack,
|
||||
isMergedNodePack
|
||||
} from '@/types/comfyManagerTypes'
|
||||
|
||||
const { nodePack, isSelected = false } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
nodePack: MergedNodePack | RegistryPack
|
||||
isSelected?: boolean
|
||||
}>()
|
||||
|
||||
@@ -136,9 +140,9 @@ const isDisabled = computed(
|
||||
|
||||
whenever(isInstalled, () => (isInstalling.value = false))
|
||||
|
||||
// TODO: remove type assertion once comfy_nodes is added to node (pack) info type in backend
|
||||
const nodesCount = computed(() => (nodePack as any).comfy_nodes?.length)
|
||||
|
||||
const nodesCount = computed(() =>
|
||||
isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined
|
||||
)
|
||||
const publisherName = computed(() => {
|
||||
if (!nodePack) return null
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<i class="pi pi-download text-muted"></i>
|
||||
<span>{{ formattedDownloads }}</span>
|
||||
</div>
|
||||
<PackInstallButton :node-packs="[nodePack]" />
|
||||
<PackInstallButton v-if="!isInstalled" :node-packs="[nodePack]" />
|
||||
<PackEnableToggle v-else :node-pack="nodePack" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,13 +15,18 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
|
||||
const { n } = useI18n()
|
||||
|
||||
const formattedDownloads = computed(() =>
|
||||
|
||||
@@ -56,7 +56,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
|
||||
import type { NodesIndexSuggestion } from '@/services/algoliaSearchService'
|
||||
import type { NodesIndexSuggestion } from '@/types/algoliaTypes'
|
||||
import {
|
||||
type SearchOption,
|
||||
SortableAlgoliaField
|
||||
|
||||
@@ -71,7 +71,6 @@ import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
@@ -189,22 +188,46 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Update the progress of the executing node
|
||||
// Update the progress of executing nodes
|
||||
watch(
|
||||
() =>
|
||||
[executionStore.executingNodeId, executionStore.executingNodeProgress] as [
|
||||
NodeId | null,
|
||||
number | null
|
||||
],
|
||||
([executingNodeId, executingNodeProgress]) => {
|
||||
() => executionStore.nodeProgressStates,
|
||||
(nodeProgressStates) => {
|
||||
// Clear progress for all nodes first
|
||||
for (const node of comfyApp.graph.nodes) {
|
||||
if (node.id == executingNodeId) {
|
||||
node.progress = executingNodeProgress ?? undefined
|
||||
} else {
|
||||
node.progress = undefined
|
||||
node.progress = undefined
|
||||
}
|
||||
|
||||
// Then set progress for nodes with progress states
|
||||
for (const nodeId in nodeProgressStates) {
|
||||
const progressState = nodeProgressStates[nodeId]
|
||||
const node = comfyApp.graph.getNodeById(progressState.display_node_id)
|
||||
|
||||
if (node && progressState) {
|
||||
// Only show progress for running nodes
|
||||
if (progressState.state === 'running') {
|
||||
if (node.progress === undefined || node.progress === 0.0) {
|
||||
console.log(
|
||||
`${Date.now()} Setting progress for node ${node.id} to ${progressState.value / progressState.max}=${progressState.value}/${progressState.max} due to ${nodeId}`
|
||||
)
|
||||
node.progress = progressState.value / progressState.max
|
||||
} else {
|
||||
// Update progress if it was already set
|
||||
console.log(
|
||||
`${Date.now()} Setting progress for node ${node.id} to Math.min(${node.progress}, ${progressState.value / progressState.max}=${progressState.value}/${progressState.max}) due to ${nodeId}`
|
||||
)
|
||||
node.progress = Math.min(
|
||||
node.progress,
|
||||
progressState.value / progressState.max
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - Do we need to force canvas redraw here?
|
||||
// comfyApp.graph.setDirtyCanvas(true, true)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Update node slot errors
|
||||
|
||||
@@ -20,33 +20,44 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
widget?: object
|
||||
nodeId: NodeId
|
||||
}>()
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const isParentNodeExecuting = ref(true)
|
||||
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
||||
|
||||
let executingNodeId: NodeId | null = null
|
||||
let parentNodeId: NodeId | null = null
|
||||
onMounted(() => {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
// Get the parent node ID from props if provided
|
||||
// For backward compatibility, fall back to the first executing node
|
||||
parentNodeId = props.nodeId
|
||||
})
|
||||
|
||||
// Watch for either a new node has starting execution or overall execution ending
|
||||
const stopWatching = watch(
|
||||
[() => executionStore.executingNode, () => executionStore.isIdle],
|
||||
[() => executionStore.executingNodeIds, () => executionStore.isIdle],
|
||||
() => {
|
||||
if (executionStore.isIdle) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if parent node is no longer in the executing nodes list
|
||||
if (
|
||||
executionStore.isIdle ||
|
||||
(executionStore.executingNode &&
|
||||
executionStore.executingNode.id !== executingNodeId)
|
||||
parentNodeId &&
|
||||
!executionStore.executingNodeIds.includes(parentNodeId)
|
||||
) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
}
|
||||
if (!executingNodeId) {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
|
||||
// Set parent node ID if not set yet
|
||||
if (!parentNodeId && executionStore.executingNodeIds.length > 0) {
|
||||
parentNodeId = executionStore.executingNodeIds[0]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
@@ -36,11 +37,35 @@ export const useBrowserTabTitle = () => {
|
||||
: DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
const nodeExecutionTitle = computed(() => {
|
||||
// Check if any nodes are in progress
|
||||
const nodeProgressEntries = Object.entries(
|
||||
executionStore.nodeProgressStates
|
||||
)
|
||||
const runningNodes = nodeProgressEntries.filter(
|
||||
([_, state]) => state.state === 'running'
|
||||
)
|
||||
|
||||
if (runningNodes.length > 0) {
|
||||
// If multiple nodes are running
|
||||
if (runningNodes.length > 1) {
|
||||
return `${executionText.value}[${runningNodes.length} ${t('g.nodesRunning', 'nodes running')}]`
|
||||
}
|
||||
// If only one node is running
|
||||
else {
|
||||
const [nodeId, state] = runningNodes[0]
|
||||
const progress = Math.round((state.value / state.max) * 100)
|
||||
const nodeType =
|
||||
executionStore.activePrompt?.workflow?.changeTracker?.activeState?.nodes.find(
|
||||
(n) => String(n.id) === nodeId
|
||||
)?.type || 'Node'
|
||||
|
||||
return `${executionText.value}[${progress}%] ${nodeType}`
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { Hit } from 'algoliasearch/dist/lite/browser'
|
||||
import { memoize, orderBy } from 'lodash'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
import { useAlgoliaSearchService } from '@/services/algoliaSearchService'
|
||||
import type {
|
||||
AlgoliaNodePack,
|
||||
SearchAttribute,
|
||||
useAlgoliaSearchService
|
||||
} from '@/services/algoliaSearchService'
|
||||
import type { NodesIndexSuggestion } from '@/services/algoliaSearchService'
|
||||
NodesIndexSuggestion,
|
||||
SearchAttribute
|
||||
} from '@/types/algoliaTypes'
|
||||
import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
|
||||
|
||||
const SEARCH_DEBOUNCE_TIME = 320
|
||||
|
||||
@@ -23,6 +23,9 @@ export const useTextPreviewWidget = (
|
||||
name: inputSpec.name,
|
||||
component: TextPreviewWidget,
|
||||
inputSpec,
|
||||
componentProps: {
|
||||
nodeId: node.id
|
||||
},
|
||||
options: {
|
||||
getValue: () => widgetValue.value,
|
||||
setValue: (value: string | object) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
@@ -1178,9 +1179,10 @@ export class GroupNodeHandler {
|
||||
node.onDrawForeground = function (ctx) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onDrawForeground?.apply?.(this, arguments)
|
||||
const executionStore = useExecutionStore()
|
||||
if (
|
||||
// @ts-expect-error fixme ts strict error
|
||||
+app.runningNodeId === this.id &&
|
||||
executionStore.nodeProgressStates[this.id] &&
|
||||
executionStore.nodeProgressStates[this.id].state === 'running' &&
|
||||
this.runningInternalNodeId !== null
|
||||
) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
@@ -1275,6 +1277,45 @@ export class GroupNodeHandler {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
(_, id) => id
|
||||
)
|
||||
/*
|
||||
// Handle progress_state events for multiple executing nodes
|
||||
const progress_state = handleEvent.call(
|
||||
this,
|
||||
'progress_state',
|
||||
(d) => {
|
||||
// Check if any of our inner nodes are in this progress state update
|
||||
for (const nodeId in d.nodes) {
|
||||
const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == nodeId);
|
||||
if (innerNodeIndex > -1) return nodeId;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(d, id, node) => {
|
||||
// Create a new progress_state event with just our group node
|
||||
const newProgressState = { ...d };
|
||||
newProgressState.nodes = { [id]: {
|
||||
node: id,
|
||||
state: 'running',
|
||||
value: 0,
|
||||
max: 1,
|
||||
prompt_id: d.prompt_id
|
||||
}};
|
||||
|
||||
// If we have a specific running internal node, update its state
|
||||
if (node.runningInternalNodeId !== null) {
|
||||
const innerNodeId = this.innerNodes[node.runningInternalNodeId].id;
|
||||
if (d.nodes[innerNodeId]) {
|
||||
newProgressState.nodes[id] = {
|
||||
...d.nodes[innerNodeId],
|
||||
node: id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return newProgressState;
|
||||
}
|
||||
);
|
||||
*/
|
||||
|
||||
const executed = handleEvent.call(
|
||||
this,
|
||||
@@ -1294,6 +1335,7 @@ export class GroupNodeHandler {
|
||||
this.node.onRemoved = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onRemoved?.apply(this, arguments)
|
||||
// api.removeEventListener('progress_state', progress_state)
|
||||
api.removeEventListener('executing', executing)
|
||||
api.removeEventListener('executed', executed)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EventManagerInterface, PreviewManagerInterface } from './interfaces'
|
||||
|
||||
export class PreviewManager implements PreviewManagerInterface {
|
||||
previewCamera: THREE.Camera
|
||||
previewContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
previewContainer: HTMLDivElement = null!
|
||||
showPreview: boolean = true
|
||||
previewWidth: number = 120
|
||||
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "Fit view to selected nodes"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "Move Selected Nodes Down"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "Move Selected Nodes Left"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "Move Selected Nodes Right"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "Move Selected Nodes Up"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "Reset View"
|
||||
},
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"copy": "Copy",
|
||||
"imageUrl": "Image URL",
|
||||
"clear": "Clear",
|
||||
"copyURL": "Copy URL"
|
||||
"copyURL": "Copy URL",
|
||||
"nodesRunning": "nodes running"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
@@ -794,6 +795,10 @@
|
||||
"Browse Templates": "Browse Templates",
|
||||
"Delete Selected Items": "Delete Selected Items",
|
||||
"Fit view to selected nodes": "Fit view to selected nodes",
|
||||
"Move Selected Nodes Down": "Move Selected Nodes Down",
|
||||
"Move Selected Nodes Left": "Move Selected Nodes Left",
|
||||
"Move Selected Nodes Right": "Move Selected Nodes Right",
|
||||
"Move Selected Nodes Up": "Move Selected Nodes Up",
|
||||
"Reset View": "Reset View",
|
||||
"Resize Selected Nodes": "Resize Selected Nodes",
|
||||
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "Ajustar vista a los nodos seleccionados"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "Mover nodos seleccionados hacia abajo"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "Mover nodos seleccionados a la izquierda"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "Mover nodos seleccionados a la derecha"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "Mover nodos seleccionados hacia arriba"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "Restablecer vista"
|
||||
},
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
"noTasksFoundMessage": "No hay tareas en la cola.",
|
||||
"noWorkflowsFound": "No se encontraron flujos de trabajo.",
|
||||
"nodes": "Nodos",
|
||||
"nodesRunning": "nodos en ejecución",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Abrir nuevo problema",
|
||||
"overwrite": "Sobrescribir",
|
||||
@@ -709,6 +710,10 @@
|
||||
"Interrupt": "Interrumpir",
|
||||
"Load Default Workflow": "Cargar flujo de trabajo predeterminado",
|
||||
"Manage group nodes": "Gestionar nodos de grupo",
|
||||
"Move Selected Nodes Down": "Mover nodos seleccionados hacia abajo",
|
||||
"Move Selected Nodes Left": "Mover nodos seleccionados hacia la izquierda",
|
||||
"Move Selected Nodes Right": "Mover nodos seleccionados hacia la derecha",
|
||||
"Move Selected Nodes Up": "Mover nodos seleccionados hacia arriba",
|
||||
"Mute/Unmute Selected Nodes": "Silenciar/Activar sonido de nodos seleccionados",
|
||||
"New": "Nuevo",
|
||||
"Next Opened Workflow": "Siguiente flujo de trabajo abierto",
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "Ajuster la vue aux nœuds sélectionnés"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "Déplacer les nœuds sélectionnés vers le bas"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "Déplacer les nœuds sélectionnés vers la gauche"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "Déplacer les nœuds sélectionnés vers la droite"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "Déplacer les nœuds sélectionnés vers le haut"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "Réinitialiser la vue"
|
||||
},
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
"noTasksFoundMessage": "Il n'y a pas de tâches dans la file d'attente.",
|
||||
"noWorkflowsFound": "Aucun flux de travail trouvé.",
|
||||
"nodes": "Nœuds",
|
||||
"nodesRunning": "nœuds en cours d’exécution",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Ouvrir un nouveau problème",
|
||||
"overwrite": "Écraser",
|
||||
@@ -709,6 +710,10 @@
|
||||
"Interrupt": "Interrompre",
|
||||
"Load Default Workflow": "Charger le flux de travail par défaut",
|
||||
"Manage group nodes": "Gérer les nœuds de groupe",
|
||||
"Move Selected Nodes Down": "Déplacer les nœuds sélectionnés vers le bas",
|
||||
"Move Selected Nodes Left": "Déplacer les nœuds sélectionnés vers la gauche",
|
||||
"Move Selected Nodes Right": "Déplacer les nœuds sélectionnés vers la droite",
|
||||
"Move Selected Nodes Up": "Déplacer les nœuds sélectionnés vers le haut",
|
||||
"Mute/Unmute Selected Nodes": "Mettre en sourdine/Activer le son des nœuds sélectionnés",
|
||||
"New": "Nouveau",
|
||||
"Next Opened Workflow": "Prochain flux de travail ouvert",
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "選択したノードにビューを合わせる"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "選択したノードを下に移動"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "選択したノードを左に移動"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "選択したノードを右に移動"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "選択したノードを上に移動"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "ビューをリセット"
|
||||
},
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
"noTasksFoundMessage": "キューにタスクがありません。",
|
||||
"noWorkflowsFound": "ワークフローが見つかりません。",
|
||||
"nodes": "ノード",
|
||||
"nodesRunning": "ノードが実行中",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "新しい問題を開く",
|
||||
"overwrite": "上書き",
|
||||
@@ -709,6 +710,10 @@
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "デフォルトワークフローを読み込む",
|
||||
"Manage group nodes": "グループノードを管理",
|
||||
"Move Selected Nodes Down": "選択したノードを下へ移動",
|
||||
"Move Selected Nodes Left": "選択したノードを左へ移動",
|
||||
"Move Selected Nodes Right": "選択したノードを右へ移動",
|
||||
"Move Selected Nodes Up": "選択したノードを上へ移動",
|
||||
"Mute/Unmute Selected Nodes": "選択したノードのミュート/ミュート解除",
|
||||
"New": "新規",
|
||||
"Next Opened Workflow": "次に開いたワークフロー",
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "선택한 노드에 뷰 맞추기"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "선택한 노드 아래로 이동"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "선택한 노드 왼쪽으로 이동"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "선택한 노드 오른쪽으로 이동"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "선택한 노드 위로 이동"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "뷰 재설정"
|
||||
},
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
"noTasksFoundMessage": "대기열에 작업이 없습니다.",
|
||||
"noWorkflowsFound": "워크플로를 찾을 수 없습니다.",
|
||||
"nodes": "노드",
|
||||
"nodesRunning": "노드 실행 중",
|
||||
"ok": "확인",
|
||||
"openNewIssue": "새 문제 열기",
|
||||
"overwrite": "덮어쓰기",
|
||||
@@ -709,6 +710,10 @@
|
||||
"Interrupt": "중단",
|
||||
"Load Default Workflow": "기본 워크플로 불러오기",
|
||||
"Manage group nodes": "그룹 노드 관리",
|
||||
"Move Selected Nodes Down": "선택한 노드 아래로 이동",
|
||||
"Move Selected Nodes Left": "선택한 노드 왼쪽으로 이동",
|
||||
"Move Selected Nodes Right": "선택한 노드 오른쪽으로 이동",
|
||||
"Move Selected Nodes Up": "선택한 노드 위로 이동",
|
||||
"Mute/Unmute Selected Nodes": "선택한 노드 활성화/비활성화",
|
||||
"New": "새로 만들기",
|
||||
"Next Opened Workflow": "다음 열린 워크플로",
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "Подогнать вид к выбранным нодам"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "Переместить выбранные узлы вниз"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "Переместить выбранные узлы влево"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "Переместить выбранные узлы вправо"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "Переместить выбранные узлы вверх"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "Сбросить вид"
|
||||
},
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
"noTasksFoundMessage": "В очереди нет задач.",
|
||||
"noWorkflowsFound": "Рабочие процессы не найдены.",
|
||||
"nodes": "Узлы",
|
||||
"nodesRunning": "запущено узлов",
|
||||
"ok": "ОК",
|
||||
"openNewIssue": "Открыть новую проблему",
|
||||
"overwrite": "Перезаписать",
|
||||
@@ -709,6 +710,10 @@
|
||||
"Interrupt": "Прервать",
|
||||
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
|
||||
"Manage group nodes": "Управление групповыми нодами",
|
||||
"Move Selected Nodes Down": "Переместить выбранные узлы вниз",
|
||||
"Move Selected Nodes Left": "Переместить выбранные узлы влево",
|
||||
"Move Selected Nodes Right": "Переместить выбранные узлы вправо",
|
||||
"Move Selected Nodes Up": "Переместить выбранные узлы вверх",
|
||||
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
|
||||
"New": "Новый",
|
||||
"Next Opened Workflow": "Следующий открытый рабочий процесс",
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "适应视图到选中节点"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "下移选中的节点"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "向左移动选中节点"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "向右移动选中节点"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "上移选中的节点"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "重置视图"
|
||||
},
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
"noTasksFoundMessage": "队列中没有任务。",
|
||||
"noWorkflowsFound": "未找到工作流。",
|
||||
"nodes": "节点",
|
||||
"nodesRunning": "节点正在运行",
|
||||
"ok": "确定",
|
||||
"openNewIssue": "打开新问题",
|
||||
"overwrite": "覆盖",
|
||||
@@ -709,6 +710,10 @@
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "加载默认工作流",
|
||||
"Manage group nodes": "管理组节点",
|
||||
"Move Selected Nodes Down": "下移所选节点",
|
||||
"Move Selected Nodes Left": "左移所选节点",
|
||||
"Move Selected Nodes Right": "右移所选节点",
|
||||
"Move Selected Nodes Up": "上移所选节点",
|
||||
"Mute/Unmute Selected Nodes": "静音/取消静音选定节点",
|
||||
"New": "新建",
|
||||
"Next Opened Workflow": "下一个打开的工作流",
|
||||
|
||||
@@ -45,6 +45,22 @@ const zProgressWsMessage = z.object({
|
||||
node: zNodeId
|
||||
})
|
||||
|
||||
const zNodeProgressState = z.object({
|
||||
value: z.number(),
|
||||
max: z.number(),
|
||||
state: z.enum(['pending', 'running', 'finished', 'error']),
|
||||
node_id: zNodeId,
|
||||
prompt_id: zPromptId,
|
||||
display_node_id: zNodeId.optional(),
|
||||
parent_node_id: zNodeId.optional(),
|
||||
real_node_id: zNodeId.optional()
|
||||
})
|
||||
|
||||
const zProgressStateWsMessage = z.object({
|
||||
prompt_id: zPromptId,
|
||||
nodes: z.record(zNodeId, zNodeProgressState)
|
||||
})
|
||||
|
||||
const zExecutingWsMessage = z.object({
|
||||
node: zNodeId,
|
||||
display_node: zNodeId,
|
||||
@@ -129,6 +145,8 @@ export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
|
||||
export type DisplayComponentWsMessage = z.infer<
|
||||
typeof zDisplayComponentWsMessage
|
||||
>
|
||||
export type NodeProgressState = z.infer<typeof zNodeProgressState>
|
||||
export type ProgressStateWsMessage = z.infer<typeof zProgressStateWsMessage>
|
||||
// End of ws messages
|
||||
|
||||
const zPromptInputItem = z.object({
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
LogsRawResponse,
|
||||
LogsWsMessage,
|
||||
PendingTaskItem,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage,
|
||||
PromptResponse,
|
||||
@@ -103,7 +104,17 @@ interface BackendApiCalls {
|
||||
logs: LogsWsMessage
|
||||
/** Binary preview/progress data */
|
||||
b_preview: Blob
|
||||
/** Binary preview with metadata (node_id, prompt_id) */
|
||||
b_preview_with_metadata: {
|
||||
blob: Blob
|
||||
nodeId: string
|
||||
parentNodeId: string
|
||||
displayNodeId: string
|
||||
realNodeId: string
|
||||
promptId: string
|
||||
}
|
||||
progress_text: ProgressTextWsMessage
|
||||
progress_state: ProgressStateWsMessage
|
||||
display_component: DisplayComponentWsMessage
|
||||
}
|
||||
|
||||
@@ -432,6 +443,33 @@ export class ComfyApi extends EventTarget {
|
||||
})
|
||||
this.dispatchCustomEvent('b_preview', imageBlob)
|
||||
break
|
||||
case 4:
|
||||
// PREVIEW_IMAGE_WITH_METADATA
|
||||
const decoder4 = new TextDecoder()
|
||||
const metadataLength = view.getUint32(4)
|
||||
const metadataBytes = event.data.slice(8, 8 + metadataLength)
|
||||
const metadata = JSON.parse(decoder4.decode(metadataBytes))
|
||||
const imageData4 = event.data.slice(8 + metadataLength)
|
||||
|
||||
let imageMime4 = metadata.image_type
|
||||
|
||||
const imageBlob4 = new Blob([imageData4], {
|
||||
type: imageMime4
|
||||
})
|
||||
|
||||
// Dispatch enhanced preview event with metadata
|
||||
this.dispatchCustomEvent('b_preview_with_metadata', {
|
||||
blob: imageBlob4,
|
||||
nodeId: metadata.node_id,
|
||||
displayNodeId: metadata.display_node_id,
|
||||
parentNodeId: metadata.parent_node_id,
|
||||
realNodeId: metadata.real_node_id,
|
||||
promptId: metadata.prompt_id
|
||||
})
|
||||
|
||||
// Also dispatch legacy b_preview for backward compatibility
|
||||
this.dispatchCustomEvent('b_preview', imageBlob4)
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown binary websocket message of type ${eventType}`
|
||||
@@ -461,6 +499,7 @@ export class ComfyApi extends EventTarget {
|
||||
case 'execution_cached':
|
||||
case 'execution_success':
|
||||
case 'progress':
|
||||
case 'progress_state':
|
||||
case 'executed':
|
||||
case 'graphChanged':
|
||||
case 'promptQueued':
|
||||
|
||||
@@ -193,6 +193,8 @@ export class ComfyApp {
|
||||
|
||||
/**
|
||||
* @deprecated Use useExecutionStore().executingNodeId instead
|
||||
* TODO: Update to support multiple executing nodes. This getter returns only the first executing node.
|
||||
* Consider updating consumers to handle multiple nodes or use executingNodeIds array.
|
||||
*/
|
||||
get runningNodeId(): NodeId | null {
|
||||
return useExecutionStore().executingNodeId
|
||||
@@ -634,10 +636,6 @@ export class ComfyApp {
|
||||
|
||||
api.addEventListener('executing', () => {
|
||||
this.graph.setDirtyCanvas(true, false)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.revokePreviews(this.runningNodeId)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
delete this.nodePreviewImages[this.runningNodeId]
|
||||
})
|
||||
|
||||
api.addEventListener('executed', ({ detail }) => {
|
||||
@@ -686,15 +684,13 @@ export class ComfyApp {
|
||||
this.canvas.draw(true, true)
|
||||
})
|
||||
|
||||
api.addEventListener('b_preview', ({ detail }) => {
|
||||
const id = this.runningNodeId
|
||||
if (id == null) return
|
||||
|
||||
const blob = detail
|
||||
api.addEventListener('b_preview_with_metadata', ({ detail }) => {
|
||||
// Enhanced preview with explicit node context
|
||||
const { blob, displayNodeId } = detail
|
||||
this.revokePreviews(displayNodeId)
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
// Ensure clean up if `executing` event is missed.
|
||||
this.revokePreviews(id)
|
||||
this.nodePreviewImages[id] = [blobUrl]
|
||||
// Preview cleanup is now handled in progress_state event to support multiple concurrent previews
|
||||
this.nodePreviewImages[displayNodeId] = [blobUrl]
|
||||
})
|
||||
|
||||
api.init()
|
||||
|
||||
@@ -237,6 +237,7 @@ export class ComponentWidgetImpl<
|
||||
component: Component
|
||||
inputSpec: InputSpec
|
||||
props?: P
|
||||
componentProps?: Record<string, unknown>
|
||||
options: DOMWidgetOptions<V>
|
||||
}) {
|
||||
super({
|
||||
@@ -245,7 +246,9 @@ export class ComponentWidgetImpl<
|
||||
})
|
||||
this.component = obj.component
|
||||
this.inputSpec = obj.inputSpec
|
||||
this.props = obj.props
|
||||
this.props = obj.componentProps
|
||||
? ({ ...obj.props, ...obj.componentProps } as P)
|
||||
: obj.props
|
||||
}
|
||||
|
||||
override computeLayoutSize() {
|
||||
|
||||
@@ -1,68 +1,26 @@
|
||||
import QuickLRU from '@alloc/quick-lru'
|
||||
import type {
|
||||
BaseSearchParamsWithoutQuery,
|
||||
Hit,
|
||||
SearchQuery,
|
||||
SearchResponse
|
||||
} from 'algoliasearch/dist/lite/browser'
|
||||
import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type {
|
||||
AlgoliaNodePack,
|
||||
NodesIndexSuggestion,
|
||||
SearchAttribute,
|
||||
SearchNodePacksParams,
|
||||
SearchPacksResult
|
||||
} from '@/types/algoliaTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { paramsToCacheKey } from '@/utils/formatUtil'
|
||||
|
||||
type RegistryNodePack = components['schemas']['Node']
|
||||
|
||||
const DEFAULT_MAX_CACHE_SIZE = 64
|
||||
const DEFAULT_MIN_CHARS_FOR_SUGGESTIONS = 2
|
||||
|
||||
type SafeNestedProperty<
|
||||
T,
|
||||
K1 extends keyof T,
|
||||
K2 extends keyof NonNullable<T[K1]>
|
||||
> = T[K1] extends undefined | null ? undefined : NonNullable<T[K1]>[K2]
|
||||
|
||||
type RegistryNodePack = components['schemas']['Node']
|
||||
type SearchPacksResult = {
|
||||
nodePacks: Hit<AlgoliaNodePack>[]
|
||||
querySuggestions: Hit<NodesIndexSuggestion>[]
|
||||
}
|
||||
|
||||
export interface AlgoliaNodePack {
|
||||
objectID: RegistryNodePack['id']
|
||||
name: RegistryNodePack['name']
|
||||
publisher_id: SafeNestedProperty<RegistryNodePack, 'publisher', 'id'>
|
||||
description: RegistryNodePack['description']
|
||||
comfy_nodes: string[]
|
||||
total_install: RegistryNodePack['downloads']
|
||||
id: RegistryNodePack['id']
|
||||
create_time: string
|
||||
update_time: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'createdAt'
|
||||
>
|
||||
license: RegistryNodePack['license']
|
||||
repository_url: RegistryNodePack['repository']
|
||||
status: RegistryNodePack['status']
|
||||
latest_version: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'version'
|
||||
>
|
||||
latest_version_status: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'status'
|
||||
>
|
||||
comfy_node_extract_status: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'comfy_node_extract_status'
|
||||
>
|
||||
icon_url: RegistryNodePack['icon']
|
||||
}
|
||||
|
||||
export type SearchAttribute = keyof AlgoliaNodePack
|
||||
|
||||
const RETRIEVE_ATTRIBUTES: SearchAttribute[] = [
|
||||
'comfy_nodes',
|
||||
'name',
|
||||
@@ -81,26 +39,6 @@ const RETRIEVE_ATTRIBUTES: SearchAttribute[] = [
|
||||
'icon_url'
|
||||
]
|
||||
|
||||
export interface NodesIndexSuggestion {
|
||||
nb_words: number
|
||||
nodes_index: {
|
||||
exact_nb_hits: number
|
||||
facets: {
|
||||
exact_matches: Record<string, number>
|
||||
analytics: Record<string, any>
|
||||
}
|
||||
}
|
||||
objectID: RegistryNodePack['id']
|
||||
popularity: number
|
||||
query: string
|
||||
}
|
||||
|
||||
type SearchNodePacksParams = BaseSearchParamsWithoutQuery & {
|
||||
pageSize: number
|
||||
pageNumber: number
|
||||
restrictSearchableAttributes: SearchAttribute[]
|
||||
}
|
||||
|
||||
interface AlgoliaSearchServiceOptions {
|
||||
/**
|
||||
* Maximum number of search results to store in the cache.
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { ComfyApp, app } from '@/scripts/app'
|
||||
import { $el } from '@/scripts/ui'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
@@ -96,7 +97,12 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
#setupStrokeStyles() {
|
||||
this.strokeStyles['running'] = function (this: LGraphNode) {
|
||||
if (this.id == app.runningNodeId) {
|
||||
const nodeProgressStates = useExecutionStore().nodeProgressStates
|
||||
const nodeId = String(this.id)
|
||||
if (
|
||||
nodeProgressStates[nodeId] &&
|
||||
nodeProgressStates[nodeId].state === 'running'
|
||||
) {
|
||||
return { color: '#0f0' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import type {
|
||||
ExecutionErrorWsMessage,
|
||||
ExecutionStartWsMessage,
|
||||
NodeError,
|
||||
NodeProgressState,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage
|
||||
} from '@/schemas/apiSchema'
|
||||
@@ -42,7 +44,22 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
|
||||
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
|
||||
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
|
||||
const executingNodeId = ref<NodeId | null>(null)
|
||||
// This is the progress of all nodes in the currently executing workflow
|
||||
const nodeProgressStates = ref<Record<string, NodeProgressState>>({})
|
||||
|
||||
// Easily access all currently executing node IDs
|
||||
const executingNodeIds = computed<NodeId[]>(() => {
|
||||
return Object.entries(nodeProgressStates)
|
||||
.filter(([_, state]) => state.state === 'running')
|
||||
.map(([nodeId, _]) => nodeId)
|
||||
})
|
||||
|
||||
// For backward compatibility - stores the primary executing node ID
|
||||
const executingNodeId = computed<NodeId | null>(() => {
|
||||
return executingNodeIds.value.length > 0 ? executingNodeIds.value[0] : null
|
||||
})
|
||||
|
||||
// For backward compatibility - returns the primary executing node
|
||||
const executingNode = computed<ComfyNode | null>(() => {
|
||||
if (!executingNodeId.value) return null
|
||||
|
||||
@@ -60,7 +77,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
)
|
||||
})
|
||||
|
||||
// This is the progress of the currently executing node, if any
|
||||
// This is the progress of the currently executing node (for backward compatibility)
|
||||
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
|
||||
const executingNodeProgress = computed(() =>
|
||||
_executingNodeProgress.value
|
||||
@@ -92,50 +109,28 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
})
|
||||
|
||||
function bindExecutionEvents() {
|
||||
api.addEventListener(
|
||||
'execution_start',
|
||||
handleExecutionStart as EventListener
|
||||
)
|
||||
api.addEventListener(
|
||||
'execution_cached',
|
||||
handleExecutionCached as EventListener
|
||||
)
|
||||
api.addEventListener('executed', handleExecuted as EventListener)
|
||||
api.addEventListener('executing', handleExecuting as EventListener)
|
||||
api.addEventListener('progress', handleProgress as EventListener)
|
||||
api.addEventListener('status', handleStatus as EventListener)
|
||||
api.addEventListener(
|
||||
'execution_error',
|
||||
handleExecutionError as EventListener
|
||||
)
|
||||
api.addEventListener('execution_start', handleExecutionStart)
|
||||
api.addEventListener('execution_cached', handleExecutionCached)
|
||||
api.addEventListener('executed', handleExecuted)
|
||||
api.addEventListener('executing', handleExecuting)
|
||||
api.addEventListener('progress', handleProgress)
|
||||
api.addEventListener('progress_state', handleProgressState)
|
||||
api.addEventListener('status', handleStatus)
|
||||
api.addEventListener('execution_error', handleExecutionError)
|
||||
}
|
||||
api.addEventListener('progress_text', handleProgressText as EventListener)
|
||||
api.addEventListener(
|
||||
'display_component',
|
||||
handleDisplayComponent as EventListener
|
||||
)
|
||||
api.addEventListener('progress_text', handleProgressText)
|
||||
api.addEventListener('display_component', handleDisplayComponent)
|
||||
|
||||
function unbindExecutionEvents() {
|
||||
api.removeEventListener(
|
||||
'execution_start',
|
||||
handleExecutionStart as EventListener
|
||||
)
|
||||
api.removeEventListener(
|
||||
'execution_cached',
|
||||
handleExecutionCached as EventListener
|
||||
)
|
||||
api.removeEventListener('executed', handleExecuted as EventListener)
|
||||
api.removeEventListener('executing', handleExecuting as EventListener)
|
||||
api.removeEventListener('progress', handleProgress as EventListener)
|
||||
api.removeEventListener('status', handleStatus as EventListener)
|
||||
api.removeEventListener(
|
||||
'execution_error',
|
||||
handleExecutionError as EventListener
|
||||
)
|
||||
api.removeEventListener(
|
||||
'progress_text',
|
||||
handleProgressText as EventListener
|
||||
)
|
||||
api.removeEventListener('execution_start', handleExecutionStart)
|
||||
api.removeEventListener('execution_cached', handleExecutionCached)
|
||||
api.removeEventListener('executed', handleExecuted)
|
||||
api.removeEventListener('executing', handleExecuting)
|
||||
api.removeEventListener('progress', handleProgress)
|
||||
api.removeEventListener('progress_state', handleProgressState)
|
||||
api.removeEventListener('status', handleStatus)
|
||||
api.removeEventListener('execution_error', handleExecutionError)
|
||||
api.removeEventListener('progress_text', handleProgressText)
|
||||
}
|
||||
|
||||
function handleExecutionStart(e: CustomEvent<ExecutionStartWsMessage>) {
|
||||
@@ -162,12 +157,8 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
|
||||
if (!activePrompt.value) return
|
||||
|
||||
if (executingNodeId.value && activePrompt.value) {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
activePrompt.value.nodes[executingNodeId.value] = true
|
||||
}
|
||||
executingNodeId.value = e.detail
|
||||
if (executingNodeId.value === null) {
|
||||
// Update the executing nodes list
|
||||
if (e.detail === null) {
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
}
|
||||
@@ -175,6 +166,37 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function handleProgressState(e: CustomEvent<ProgressStateWsMessage>) {
|
||||
const { nodes } = e.detail
|
||||
|
||||
// Revoke previews for nodes that are starting to execute
|
||||
for (const nodeId in nodes) {
|
||||
const nodeState = nodes[nodeId]
|
||||
if (nodeState.state === 'running' && !nodeProgressStates.value[nodeId]) {
|
||||
// This node just started executing, revoke its previews
|
||||
// Note that we're doing the *actual* node id instead of the display node id
|
||||
// here intentionally. That way, we don't clear the preview every time a new node
|
||||
// within an expanded graph starts executing.
|
||||
app.revokePreviews(nodeId)
|
||||
delete app.nodePreviewImages[nodeId]
|
||||
}
|
||||
}
|
||||
|
||||
// Update the progress states for all nodes
|
||||
nodeProgressStates.value = nodes
|
||||
|
||||
// If we have progress for the currently executing node, update it for backwards compatibility
|
||||
if (executingNodeId.value && nodes[executingNodeId.value]) {
|
||||
const nodeState = nodes[executingNodeId.value]
|
||||
_executingNodeProgress.value = {
|
||||
value: nodeState.value,
|
||||
max: nodeState.max,
|
||||
prompt_id: nodeState.prompt_id,
|
||||
node: nodeState.display_node_id || nodeState.node_id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleProgress(e: CustomEvent<ProgressWsMessage>) {
|
||||
_executingNodeProgress.value = e.detail
|
||||
}
|
||||
@@ -184,7 +206,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
clientId.value = api.clientId
|
||||
|
||||
// Once we've received the clientId we no longer need to listen
|
||||
api.removeEventListener('status', handleStatus as EventListener)
|
||||
api.removeEventListener('status', handleStatus)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,9 +284,13 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
*/
|
||||
lastExecutionError,
|
||||
/**
|
||||
* The id of the node that is currently being executed
|
||||
* The id of the node that is currently being executed (backward compatibility)
|
||||
*/
|
||||
executingNodeId,
|
||||
/**
|
||||
* The list of all nodes that are currently executing
|
||||
*/
|
||||
executingNodeIds,
|
||||
/**
|
||||
* The prompt that is currently being executed
|
||||
*/
|
||||
@@ -282,13 +308,17 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
*/
|
||||
executionProgress,
|
||||
/**
|
||||
* The node that is currently being executed
|
||||
* The node that is currently being executed (backward compatibility)
|
||||
*/
|
||||
executingNode,
|
||||
/**
|
||||
* The progress of the executing node (if the node reports progress)
|
||||
* The progress of the executing node (backward compatibility)
|
||||
*/
|
||||
executingNodeProgress,
|
||||
/**
|
||||
* All node progress states from progress_state events
|
||||
*/
|
||||
nodeProgressStates,
|
||||
bindExecutionEvents,
|
||||
unbindExecutionEvents,
|
||||
storePrompt,
|
||||
|
||||
74
src/types/algoliaTypes.ts
Normal file
74
src/types/algoliaTypes.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type {
|
||||
BaseSearchParamsWithoutQuery,
|
||||
Hit
|
||||
} from 'algoliasearch/dist/lite/browser'
|
||||
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type SafeNestedProperty<
|
||||
T,
|
||||
K1 extends keyof T,
|
||||
K2 extends keyof NonNullable<T[K1]>
|
||||
> = T[K1] extends undefined | null ? undefined : NonNullable<T[K1]>[K2]
|
||||
|
||||
type RegistryNodePack = components['schemas']['Node']
|
||||
export type SearchPacksResult = {
|
||||
nodePacks: Hit<AlgoliaNodePack>[]
|
||||
querySuggestions: Hit<NodesIndexSuggestion>[]
|
||||
}
|
||||
|
||||
export interface AlgoliaNodePack {
|
||||
objectID: RegistryNodePack['id']
|
||||
name: RegistryNodePack['name']
|
||||
publisher_id: SafeNestedProperty<RegistryNodePack, 'publisher', 'id'>
|
||||
description: RegistryNodePack['description']
|
||||
comfy_nodes: string[]
|
||||
total_install: RegistryNodePack['downloads']
|
||||
id: RegistryNodePack['id']
|
||||
create_time: string
|
||||
update_time: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'createdAt'
|
||||
>
|
||||
license: RegistryNodePack['license']
|
||||
repository_url: RegistryNodePack['repository']
|
||||
status: RegistryNodePack['status']
|
||||
latest_version: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'version'
|
||||
>
|
||||
latest_version_status: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'status'
|
||||
>
|
||||
comfy_node_extract_status: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'comfy_node_extract_status'
|
||||
>
|
||||
icon_url: RegistryNodePack['icon']
|
||||
}
|
||||
|
||||
export type SearchAttribute = keyof AlgoliaNodePack
|
||||
export interface NodesIndexSuggestion {
|
||||
nb_words: number
|
||||
nodes_index: {
|
||||
exact_nb_hits: number
|
||||
facets: {
|
||||
exact_matches: Record<string, number>
|
||||
analytics: Record<string, any>
|
||||
}
|
||||
}
|
||||
objectID: RegistryNodePack['id']
|
||||
popularity: number
|
||||
query: string
|
||||
}
|
||||
|
||||
export type SearchNodePacksParams = BaseSearchParamsWithoutQuery & {
|
||||
pageSize: number
|
||||
pageNumber: number
|
||||
restrictSearchableAttributes: SearchAttribute[]
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { AlgoliaNodePack } from '@/types/algoliaTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type RegistryPack = components['schemas']['Node']
|
||||
type WorkflowNodeProperties = ComfyWorkflowJSON['nodes'][0]['properties']
|
||||
|
||||
export type RegistryPack = components['schemas']['Node']
|
||||
export type MergedNodePack = RegistryPack & AlgoliaNodePack
|
||||
export const isMergedNodePack = (
|
||||
nodePack: RegistryPack | AlgoliaNodePack
|
||||
): nodePack is MergedNodePack => 'comfy_nodes' in nodePack
|
||||
|
||||
export type PackField = keyof RegistryPack | null
|
||||
|
||||
export const IsInstallingKey: InjectionKey<Ref<boolean>> =
|
||||
|
||||
@@ -4,12 +4,12 @@ import type { InjectionKey, ModelRef } from 'vue'
|
||||
|
||||
export interface TreeNode extends PrimeVueTreeNode {
|
||||
label: string
|
||||
children?: TreeNode[]
|
||||
children?: this[]
|
||||
}
|
||||
|
||||
export interface TreeExplorerNode<T = any> extends TreeNode {
|
||||
data?: T
|
||||
children?: TreeExplorerNode<T>[]
|
||||
children?: this[]
|
||||
icon?: string
|
||||
/**
|
||||
* Function to override what icon to use for the node.
|
||||
@@ -62,7 +62,7 @@ export interface TreeExplorerNode<T = any> extends TreeNode {
|
||||
}
|
||||
|
||||
export interface RenderedTreeExplorerNode<T = any> extends TreeExplorerNode<T> {
|
||||
children?: RenderedTreeExplorerNode<T>[]
|
||||
children?: this[]
|
||||
icon: string
|
||||
type: 'folder' | 'node'
|
||||
/** Total number of leaves in the subtree */
|
||||
|
||||
@@ -61,8 +61,8 @@ const mergeNumericInputSpec = <T extends IntInputSpec | FloatInputSpec>(
|
||||
}
|
||||
|
||||
return mergeCommonInputSpec(
|
||||
[type, { ...options1, ...mergedOptions }] as unknown as T,
|
||||
[type, { ...options2, ...mergedOptions }] as unknown as T
|
||||
[type, { ...options1, ...mergedOptions }] as T,
|
||||
[type, { ...options2, ...mergedOptions }] as T
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ const mergeComboInputSpec = <T extends ComboInputSpec | ComboInputSpecV2>(
|
||||
}
|
||||
|
||||
return mergeCommonInputSpec(
|
||||
['COMBO', { ...options1, options: intersection }] as unknown as T,
|
||||
['COMBO', { ...options2, options: intersection }] as unknown as T
|
||||
['COMBO', { ...options1, options: intersection }] as T,
|
||||
['COMBO', { ...options2, options: intersection }] as T
|
||||
)
|
||||
}
|
||||
|
||||
@@ -107,9 +107,7 @@ const mergeCommonInputSpec = <T extends InputSpec>(
|
||||
return value1 === value2 || (_.isNil(value1) && _.isNil(value2))
|
||||
})
|
||||
|
||||
return mergeIsValid
|
||||
? ([type, { ...options1, ...options2 }] as unknown as T)
|
||||
: null
|
||||
return mergeIsValid ? ([type, { ...options1, ...options2 }] as T) : null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -116,7 +116,7 @@ export const findNodeByKey = <T extends TreeNode>(
|
||||
return null
|
||||
}
|
||||
for (const child of root.children) {
|
||||
const result = findNodeByKey(child as T, key)
|
||||
const result = findNodeByKey(child, key)
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
@@ -130,11 +130,11 @@ export const findNodeByKey = <T extends TreeNode>(
|
||||
* @returns A deep clone of the node.
|
||||
*/
|
||||
export function cloneTree<T extends TreeNode>(node: T): T {
|
||||
const clone: T = { ...node } as T
|
||||
const clone = { ...node }
|
||||
|
||||
// Clone children recursively
|
||||
if (node.children && node.children.length > 0) {
|
||||
clone.children = node.children.map((child) => cloneTree(child as T))
|
||||
clone.children = node.children.map((child) => cloneTree(child))
|
||||
}
|
||||
|
||||
return clone
|
||||
|
||||
@@ -3,12 +3,20 @@ import { nextTick, reactive } from 'vue'
|
||||
|
||||
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
||||
|
||||
// Mock i18n module
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string, fallback: string) =>
|
||||
key === 'g.nodesRunning' ? 'nodes running' : fallback
|
||||
}))
|
||||
|
||||
// Mock the execution store
|
||||
const executionStore = reactive({
|
||||
isIdle: true,
|
||||
executionProgress: 0,
|
||||
executingNode: null as any,
|
||||
executingNodeProgress: 0
|
||||
executingNodeProgress: 0,
|
||||
nodeProgressStates: {} as any,
|
||||
activePrompt: null as any
|
||||
})
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: () => executionStore
|
||||
@@ -37,6 +45,8 @@ describe('useBrowserTabTitle', () => {
|
||||
executionStore.executionProgress = 0
|
||||
executionStore.executingNode = null as any
|
||||
executionStore.executingNodeProgress = 0
|
||||
executionStore.nodeProgressStates = {}
|
||||
executionStore.activePrompt = null
|
||||
|
||||
// reset setting and workflow stores
|
||||
;(settingStore.get as any).mockReturnValue('Enabled')
|
||||
@@ -97,13 +107,41 @@ describe('useBrowserTabTitle', () => {
|
||||
expect(document.title).toBe('[30%]ComfyUI')
|
||||
})
|
||||
|
||||
it('shows node execution title when executing a node', async () => {
|
||||
it('shows node execution title when executing a node using nodeProgressStates', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.executingNodeProgress = 0.5
|
||||
executionStore.executingNode = { type: 'Foo' }
|
||||
executionStore.nodeProgressStates = {
|
||||
'1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
|
||||
}
|
||||
executionStore.activePrompt = {
|
||||
workflow: {
|
||||
changeTracker: {
|
||||
activeState: {
|
||||
nodes: [{ id: 1, type: 'Foo' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][50%] Foo')
|
||||
})
|
||||
|
||||
it('shows multiple nodes running when multiple nodes are executing', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.nodeProgressStates = {
|
||||
'1': {
|
||||
state: 'running',
|
||||
value: 5,
|
||||
max: 10,
|
||||
node: '1',
|
||||
prompt_id: 'test'
|
||||
},
|
||||
'2': { state: 'running', value: 8, max: 10, node: '2', prompt_id: 'test' }
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][2 nodes running]')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,10 +3,9 @@ import dotenv from 'dotenv'
|
||||
import IconsResolver from 'unplugin-icons/resolver'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import { type UserConfig, defineConfig } from 'vite'
|
||||
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import type { UserConfigExport } from 'vitest/config'
|
||||
|
||||
import {
|
||||
addElementVnodeExportPlugin,
|
||||
@@ -154,4 +153,4 @@ export default defineConfig({
|
||||
optimizeDeps: {
|
||||
exclude: ['@comfyorg/litegraph', '@comfyorg/comfyui-electron-types']
|
||||
}
|
||||
}) as UserConfigExport
|
||||
}) satisfies UserConfig as UserConfig
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Plugin, defineConfig } from 'vite'
|
||||
import { mergeConfig } from 'vite'
|
||||
import type { UserConfig } from 'vitest/config'
|
||||
|
||||
import baseConfig from './vite.config.mts'
|
||||
|
||||
@@ -83,7 +82,7 @@ const mockElectronAPI: Plugin = {
|
||||
}
|
||||
|
||||
export default mergeConfig(
|
||||
baseConfig as unknown as UserConfig,
|
||||
baseConfig,
|
||||
defineConfig({
|
||||
plugins: [mockElectronAPI]
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user