mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
Compare commits
5 Commits
core/1.32
...
edit-node-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69366e5e91 | ||
|
|
5facc77529 | ||
|
|
289cbe428e | ||
|
|
96e39b4c85 | ||
|
|
590280dd92 |
@@ -3,76 +3,117 @@
|
|||||||
class="selection-toolbox absolute left-1/2 rounded-lg"
|
class="selection-toolbox absolute left-1/2 rounded-lg"
|
||||||
:pt="{
|
:pt="{
|
||||||
header: 'hidden',
|
header: 'hidden',
|
||||||
content: 'p-0 flex flex-row'
|
content: 'p-0'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ColorPickerButton v-show="nodeSelected || groupSelected" />
|
<div class="flex flex-col">
|
||||||
<Button
|
<div class="flex flex-row">
|
||||||
v-show="nodeSelected"
|
<ColorPickerButton v-show="nodeSelected || groupSelected" />
|
||||||
severity="secondary"
|
<Button
|
||||||
text
|
v-show="nodeSelected"
|
||||||
@click="
|
severity="secondary"
|
||||||
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
|
text
|
||||||
"
|
@click="
|
||||||
data-testid="bypass-button"
|
() =>
|
||||||
>
|
commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
|
||||||
<template #icon>
|
"
|
||||||
<i-game-icons:detour />
|
data-testid="bypass-button"
|
||||||
</template>
|
>
|
||||||
</Button>
|
<template #icon>
|
||||||
<Button
|
<i-game-icons:detour />
|
||||||
v-show="nodeSelected || groupSelected"
|
</template>
|
||||||
severity="secondary"
|
</Button>
|
||||||
text
|
<Button
|
||||||
icon="pi pi-thumbtack"
|
v-show="nodeSelected || groupSelected"
|
||||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
|
severity="secondary"
|
||||||
/>
|
text
|
||||||
<Button
|
icon="pi pi-thumbtack"
|
||||||
severity="danger"
|
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
|
||||||
text
|
/>
|
||||||
icon="pi pi-trash"
|
<Button
|
||||||
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
|
severity="danger"
|
||||||
/>
|
text
|
||||||
<Button
|
icon="pi pi-trash"
|
||||||
v-show="isRefreshable"
|
@click="
|
||||||
severity="info"
|
() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')
|
||||||
text
|
"
|
||||||
icon="pi pi-refresh"
|
/>
|
||||||
@click="refreshSelected"
|
<Button
|
||||||
/>
|
v-show="isRefreshable"
|
||||||
<Button
|
severity="info"
|
||||||
v-for="command in extensionToolboxCommands"
|
text
|
||||||
:key="command.id"
|
icon="pi pi-refresh"
|
||||||
severity="secondary"
|
@click="refreshSelected"
|
||||||
text
|
/>
|
||||||
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
|
<Button
|
||||||
@click="() => commandStore.execute(command.id)"
|
v-for="command in extensionToolboxCommands"
|
||||||
/>
|
:key="command.id"
|
||||||
|
severity="secondary"
|
||||||
|
text
|
||||||
|
:icon="
|
||||||
|
typeof command.icon === 'function' ? command.icon() : command.icon
|
||||||
|
"
|
||||||
|
@click="() => commandStore.execute(command.id)"
|
||||||
|
/>
|
||||||
|
<Divider
|
||||||
|
layout="vertical"
|
||||||
|
class="mx-1 my-2"
|
||||||
|
v-if="hasAdvancedOptions"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="hasAdvancedOptions"
|
||||||
|
severity="secondary"
|
||||||
|
text
|
||||||
|
:icon="showAdvancedOptions ? 'pi pi-chevron-up' : 'pi pi-ellipsis-h'"
|
||||||
|
@click="showAdvancedOptions = !showAdvancedOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="showAdvancedOptions" class="flex flex-row">
|
||||||
|
<NodeModelsButton
|
||||||
|
v-if="modelNodeSelected && canvasStore.selectedItems.length === 1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
|
import Divider from 'primevue/divider'
|
||||||
import Panel from 'primevue/panel'
|
import Panel from 'primevue/panel'
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||||
|
import NodeModelsButton from '@/components/graph/selectionToolbox/nodeModelsMetadata/NodeModelsButton.vue'
|
||||||
import { useRefreshableSelection } from '@/composables/useRefreshableSelection'
|
import { useRefreshableSelection } from '@/composables/useRefreshableSelection'
|
||||||
import { useExtensionService } from '@/services/extensionService'
|
import { useExtensionService } from '@/services/extensionService'
|
||||||
import { ComfyCommand, useCommandStore } from '@/stores/commandStore'
|
import { ComfyCommand, useCommandStore } from '@/stores/commandStore'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
import { isLGraphGroup, isLGraphNode, isModelNode } from '@/utils/litegraphUtil'
|
||||||
|
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const extensionService = useExtensionService()
|
const extensionService = useExtensionService()
|
||||||
const { isRefreshable, refreshSelected } = useRefreshableSelection()
|
const { isRefreshable, refreshSelected } = useRefreshableSelection()
|
||||||
const nodeSelected = computed(() =>
|
const showAdvancedOptions = ref(false)
|
||||||
canvasStore.selectedItems.some(isLGraphNode)
|
|
||||||
|
const selectedNodes = computed(() =>
|
||||||
|
canvasStore.selectedItems.filter(isLGraphNode)
|
||||||
)
|
)
|
||||||
const groupSelected = computed(() =>
|
const selectedModelNodes = computed(() =>
|
||||||
canvasStore.selectedItems.some(isLGraphGroup)
|
selectedNodes.value.filter(isModelNode)
|
||||||
)
|
)
|
||||||
|
const selectedGroups = computed(() =>
|
||||||
|
canvasStore.selectedItems.filter(isLGraphGroup)
|
||||||
|
)
|
||||||
|
const nodeSelected = computed(() => selectedNodes.value.length > 0)
|
||||||
|
const groupSelected = computed(() => selectedGroups.value.length > 0)
|
||||||
|
const modelNodeSelected = computed(() => selectedModelNodes.value.length > 0)
|
||||||
|
|
||||||
|
const showModelMetadataTool = computed(
|
||||||
|
() => modelNodeSelected.value && canvasStore.selectedItems.length === 1
|
||||||
|
)
|
||||||
|
const hasAdvancedOptions = computed(() => showModelMetadataTool.value)
|
||||||
|
|
||||||
const extensionToolboxCommands = computed<ComfyCommand[]>(() => {
|
const extensionToolboxCommands = computed<ComfyCommand[]>(() => {
|
||||||
const commandIds = new Set<string>(
|
const commandIds = new Set<string>(
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
v-slot="$form"
|
||||||
|
:resolver="zodResolver(zModelFile)"
|
||||||
|
:initial-values="node.properties?.models?.[index]"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="p-2 surface-ground rounded-lg">
|
||||||
|
<Message
|
||||||
|
v-if="$form.name?.error || $form.directory?.error"
|
||||||
|
severity="error"
|
||||||
|
size="small"
|
||||||
|
variant="simple"
|
||||||
|
>
|
||||||
|
{{ $form.name?.error?.message || $form.directory?.error?.message }}
|
||||||
|
</Message>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon>
|
||||||
|
<i class="pi pi-file" />
|
||||||
|
</InputGroupAddon>
|
||||||
|
<FormField v-slot="$field" name="name">
|
||||||
|
<IftaLabel>
|
||||||
|
<InputText
|
||||||
|
v-bind="$field"
|
||||||
|
:inputId="`model-name-${index}`"
|
||||||
|
class="h-full"
|
||||||
|
/>
|
||||||
|
<label :for="`model-name-${index}`">
|
||||||
|
{{ $t('nodeMetadata.models.fields.filename') }}
|
||||||
|
</label>
|
||||||
|
</IftaLabel>
|
||||||
|
</FormField>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon>
|
||||||
|
<i class="pi pi-folder" />
|
||||||
|
</InputGroupAddon>
|
||||||
|
<FormField v-slot="$field" name="directory">
|
||||||
|
<IftaLabel>
|
||||||
|
<InputText
|
||||||
|
v-bind="$field"
|
||||||
|
:inputId="`model-directory-${index}`"
|
||||||
|
class="h-full"
|
||||||
|
/>
|
||||||
|
<label :for="`model-directory-${index}`">
|
||||||
|
{{ $t('nodeMetadata.models.fields.directory') }}
|
||||||
|
</label>
|
||||||
|
</IftaLabel>
|
||||||
|
</FormField>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon>
|
||||||
|
<i class="pi pi-link" />
|
||||||
|
</InputGroupAddon>
|
||||||
|
<FormField v-slot="$field" name="url">
|
||||||
|
<IftaLabel>
|
||||||
|
<InputText v-bind="$field" :inputId="`model-url-${index}`" />
|
||||||
|
<label :for="`model-url-${index}`">
|
||||||
|
{{ $t('nodeMetadata.models.fields.url') }}
|
||||||
|
</label>
|
||||||
|
</IftaLabel>
|
||||||
|
</FormField>
|
||||||
|
</InputGroup>
|
||||||
|
<Message
|
||||||
|
v-if="$form.url?.error"
|
||||||
|
severity="error"
|
||||||
|
size="small"
|
||||||
|
variant="simple"
|
||||||
|
>
|
||||||
|
{{ $form.url?.error?.message }}
|
||||||
|
</Message>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-1 flex justify-center gap-2">
|
||||||
|
<Button
|
||||||
|
v-tooltip="$t('nodeMetadata.models.remove')"
|
||||||
|
icon="pi pi-minus"
|
||||||
|
severity="danger"
|
||||||
|
text
|
||||||
|
size="small"
|
||||||
|
@click="emit('remove')"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="isLast"
|
||||||
|
v-tooltip="$t('nodeMetadata.models.add')"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
text
|
||||||
|
size="small"
|
||||||
|
@click="emit('add')"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-check"
|
||||||
|
severity="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="!$form.valid"
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Form, FormField } from '@primevue/forms'
|
||||||
|
// @ts-expect-error https://github.com/primefaces/primevue/issues/6722
|
||||||
|
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||||
|
import { InputGroup } from 'primevue'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import IftaLabel from 'primevue/iftalabel'
|
||||||
|
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||||
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Message from 'primevue/message'
|
||||||
|
import { useToast } from 'primevue/usetoast'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { type ModelFile, zModelFile } from '@/schemas/comfyWorkflowSchema'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { nodeId, index } = defineProps<{
|
||||||
|
index: number
|
||||||
|
isLast: boolean
|
||||||
|
nodeId: string | number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'remove'): void
|
||||||
|
(e: 'add'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const node = computed(() => app.graph.getNodeById(nodeId))
|
||||||
|
|
||||||
|
const handleSubmit = (event: { values: ModelFile; valid: boolean }) => {
|
||||||
|
if (!event.valid) return
|
||||||
|
|
||||||
|
node.value.properties ||= {}
|
||||||
|
node.value.properties.models ||= []
|
||||||
|
node.value.properties.models[index] = event.values
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('nodeMetadata.models.success'),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
severity="secondary"
|
||||||
|
text
|
||||||
|
@click="overlayRef?.toggle($event)"
|
||||||
|
v-tooltip.top="$t('nodeMetadata.models.title')"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i class="pi pi-box" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
<OverlayPanel ref="overlayRef" class="surface-card">
|
||||||
|
<NodeModelsPopover />
|
||||||
|
</OverlayPanel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import OverlayPanel from 'primevue/overlaypanel'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import NodeModelsPopover from '@/components/graph/selectionToolbox/nodeModelsMetadata/NodeModelsPopover.vue'
|
||||||
|
|
||||||
|
const overlayRef = ref()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<template v-if="node">
|
||||||
|
<div ref="contentRef" class="flex flex-col overflow-y-auto px-4 py-2">
|
||||||
|
<div class="mb-2 flex items-center justify-between">
|
||||||
|
<span>{{ $t('nodeMetadata.models.title') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<template
|
||||||
|
v-for="(model, index) of nodeModels"
|
||||||
|
:key="`${model.name}${model.url || model.hash}`"
|
||||||
|
>
|
||||||
|
<ModelForm
|
||||||
|
:model="model"
|
||||||
|
:index="index"
|
||||||
|
:node-id="node.id"
|
||||||
|
:is-last="isLastModel(index)"
|
||||||
|
@remove="removeModel(index)"
|
||||||
|
@add="addEmptyModel"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div v-if="nodeModels.length === 0" class="flex items-center">
|
||||||
|
<div class="flex-1 flex justify-center">
|
||||||
|
<Button
|
||||||
|
v-tooltip="$t('nodeMetadata.models.add')"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
text
|
||||||
|
size="small"
|
||||||
|
@click="addEmptyModel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { LGraphNode } from '@comfyorg/litegraph'
|
||||||
|
import { useResizeObserver } from '@vueuse/core'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import ModelForm from '@/components/graph/selectionToolbox/nodeModelsMetadata/ModelForm.vue'
|
||||||
|
import type { ModelFile } from '@/schemas/comfyWorkflowSchema'
|
||||||
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
import { isModelNode } from '@/utils/litegraphUtil'
|
||||||
|
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
|
const contentRef = ref<HTMLElement>()
|
||||||
|
const nodeModels = ref<ModelFile[]>([])
|
||||||
|
const node = ref<LGraphNode | null>(null)
|
||||||
|
|
||||||
|
const isLastModel = (index: number) => index === nodeModels.value.length - 1
|
||||||
|
const formatMaxHeight = (top: number) => `calc(100vh - ${top}px)`
|
||||||
|
|
||||||
|
const addEmptyModel = () => {
|
||||||
|
nodeModels.value.push({ name: '', url: '', directory: '' })
|
||||||
|
}
|
||||||
|
const removeModel = (index: number) => {
|
||||||
|
nodeModels.value.splice(index, 1)
|
||||||
|
const models = node.value?.properties?.models as ModelFile[]
|
||||||
|
if (models) models.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
useResizeObserver(contentRef, () => {
|
||||||
|
if (contentRef.value) {
|
||||||
|
contentRef.value.style.maxHeight = formatMaxHeight(
|
||||||
|
contentRef.value.getBoundingClientRect().top
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const nodes = canvasStore.selectedItems.filter(isModelNode)
|
||||||
|
node.value = nodes[0]
|
||||||
|
|
||||||
|
if (node.value?.properties?.models) {
|
||||||
|
nodeModels.value = [...(node.value.properties.models as ModelFile[])]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -80,6 +80,19 @@
|
|||||||
"choose_file_to_upload": "choose file to upload",
|
"choose_file_to_upload": "choose file to upload",
|
||||||
"capture": "capture"
|
"capture": "capture"
|
||||||
},
|
},
|
||||||
|
"nodeMetadata": {
|
||||||
|
"models": {
|
||||||
|
"title": "Edit Node Models Metadata",
|
||||||
|
"add": "Add Model",
|
||||||
|
"remove": "Remove Model",
|
||||||
|
"success": "Node models metadata updated",
|
||||||
|
"fields": {
|
||||||
|
"filename": "Filename",
|
||||||
|
"directory": "Directory",
|
||||||
|
"url": "URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"issueReport": {
|
"issueReport": {
|
||||||
"submitErrorReport": "Submit Error Report (Optional)",
|
"submitErrorReport": "Submit Error Report (Optional)",
|
||||||
"provideEmail": "Give us your email (optional)",
|
"provideEmail": "Give us your email (optional)",
|
||||||
|
|||||||
@@ -515,6 +515,19 @@
|
|||||||
"video": "vidéo",
|
"video": "vidéo",
|
||||||
"video_models": "modèles_vidéo"
|
"video_models": "modèles_vidéo"
|
||||||
},
|
},
|
||||||
|
"nodeMetadata": {
|
||||||
|
"models": {
|
||||||
|
"add": "Ajouter un modèle",
|
||||||
|
"fields": {
|
||||||
|
"directory": "Répertoire",
|
||||||
|
"filename": "Nom de fichier",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"remove": "Supprimer le modèle",
|
||||||
|
"success": "Métadonnées des modèles de nœuds mises à jour",
|
||||||
|
"title": "Modifier les métadonnées des modèles de nœuds"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodeTemplates": {
|
"nodeTemplates": {
|
||||||
"enterName": "Entrez le nom",
|
"enterName": "Entrez le nom",
|
||||||
"saveAsTemplate": "Enregistrer comme modèle"
|
"saveAsTemplate": "Enregistrer comme modèle"
|
||||||
|
|||||||
@@ -515,6 +515,19 @@
|
|||||||
"video": "ビデオ",
|
"video": "ビデオ",
|
||||||
"video_models": "ビデオモデル"
|
"video_models": "ビデオモデル"
|
||||||
},
|
},
|
||||||
|
"nodeMetadata": {
|
||||||
|
"models": {
|
||||||
|
"add": "モデルを追加",
|
||||||
|
"fields": {
|
||||||
|
"directory": "ディレクトリ",
|
||||||
|
"filename": "ファイル名",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"remove": "モデルを削除",
|
||||||
|
"success": "ノードモデルのメタデータが更新されました",
|
||||||
|
"title": "ノードモデルメタデータの編集"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodeTemplates": {
|
"nodeTemplates": {
|
||||||
"enterName": "名前を入力",
|
"enterName": "名前を入力",
|
||||||
"saveAsTemplate": "テンプレートとして保存"
|
"saveAsTemplate": "テンプレートとして保存"
|
||||||
|
|||||||
@@ -515,6 +515,19 @@
|
|||||||
"video": "비디오",
|
"video": "비디오",
|
||||||
"video_models": "비디오 모델"
|
"video_models": "비디오 모델"
|
||||||
},
|
},
|
||||||
|
"nodeMetadata": {
|
||||||
|
"models": {
|
||||||
|
"add": "모델 추가",
|
||||||
|
"fields": {
|
||||||
|
"directory": "디렉토리",
|
||||||
|
"filename": "파일명",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"remove": "모델 제거",
|
||||||
|
"success": "노드 모델 메타데이터가 업데이트되었습니다",
|
||||||
|
"title": "노드 모델 메타데이터 편집"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodeTemplates": {
|
"nodeTemplates": {
|
||||||
"enterName": "이름 입력",
|
"enterName": "이름 입력",
|
||||||
"saveAsTemplate": "템플릿으로 저장"
|
"saveAsTemplate": "템플릿으로 저장"
|
||||||
|
|||||||
@@ -515,6 +515,19 @@
|
|||||||
"video": "видео",
|
"video": "видео",
|
||||||
"video_models": "видеомодели"
|
"video_models": "видеомодели"
|
||||||
},
|
},
|
||||||
|
"nodeMetadata": {
|
||||||
|
"models": {
|
||||||
|
"add": "Добавить модель",
|
||||||
|
"fields": {
|
||||||
|
"directory": "Директория",
|
||||||
|
"filename": "Имя файла",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"remove": "Удалить модель",
|
||||||
|
"success": "Метаданные моделей узла обновлены",
|
||||||
|
"title": "Редактирование метаданных моделей узлов"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodeTemplates": {
|
"nodeTemplates": {
|
||||||
"enterName": "Введите название",
|
"enterName": "Введите название",
|
||||||
"saveAsTemplate": "Сохранить как шаблон"
|
"saveAsTemplate": "Сохранить как шаблон"
|
||||||
|
|||||||
@@ -515,6 +515,19 @@
|
|||||||
"video": "视频",
|
"video": "视频",
|
||||||
"video_models": "视频模型"
|
"video_models": "视频模型"
|
||||||
},
|
},
|
||||||
|
"nodeMetadata": {
|
||||||
|
"models": {
|
||||||
|
"add": "添加模型",
|
||||||
|
"fields": {
|
||||||
|
"directory": "目录",
|
||||||
|
"filename": "文件名",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"remove": "移除模型",
|
||||||
|
"success": "节点模型元数据已更新",
|
||||||
|
"title": "编辑节点模型元数据"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodeTemplates": {
|
"nodeTemplates": {
|
||||||
"enterName": "输入名称",
|
"enterName": "输入名称",
|
||||||
"saveAsTemplate": "另存为模板"
|
"saveAsTemplate": "另存为模板"
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ const zVector2 = z.union([
|
|||||||
])
|
])
|
||||||
|
|
||||||
// Definition of an AI model file used in the workflow.
|
// Definition of an AI model file used in the workflow.
|
||||||
const zModelFile = z.object({
|
export const zModelFile = z.object({
|
||||||
name: z.string(),
|
name: z.string().min(1, 'Name cannot be empty'),
|
||||||
url: z.string().url(),
|
url: z.string().url(),
|
||||||
hash: z.string().optional(),
|
hash: z.string().optional(),
|
||||||
hash_type: z.string().optional(),
|
hash_type: z.string().optional(),
|
||||||
directory: z.string()
|
directory: z.string().min(1, 'Directory cannot be empty')
|
||||||
})
|
})
|
||||||
|
|
||||||
const zGraphState = z
|
const zGraphState = z
|
||||||
|
|||||||
@@ -8,11 +8,33 @@ import {
|
|||||||
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import { ModelFile } from '@/schemas/comfyWorkflowSchema'
|
||||||
|
|
||||||
type ImageNode = LGraphNode & { imgs: HTMLImageElement[] | undefined }
|
type ImageNode = LGraphNode & { imgs: HTMLImageElement[] | undefined }
|
||||||
type VideoNode = LGraphNode & {
|
type VideoNode = LGraphNode & {
|
||||||
videoContainer: HTMLElement | undefined
|
videoContainer: HTMLElement | undefined
|
||||||
imgs: HTMLVideoElement[] | undefined
|
imgs: HTMLVideoElement[] | undefined
|
||||||
}
|
}
|
||||||
|
type ModelNode = LGraphNode & {
|
||||||
|
properties: {
|
||||||
|
models?: ModelFile[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODEL_OUTPUT_TYPES = new Set([
|
||||||
|
'MODEL',
|
||||||
|
'CLIP',
|
||||||
|
'VAE',
|
||||||
|
'CONTROL_NET',
|
||||||
|
'UPSCALE_MODEL',
|
||||||
|
'CLIP_VISION',
|
||||||
|
'STYLE_MODEL',
|
||||||
|
'GLIGEN',
|
||||||
|
'HOOKS',
|
||||||
|
'UPSCALE_MODEL',
|
||||||
|
'PHOTOMAKER',
|
||||||
|
'SAM_MODEL'
|
||||||
|
])
|
||||||
|
|
||||||
export function isImageNode(node: LGraphNode | undefined): node is ImageNode {
|
export function isImageNode(node: LGraphNode | undefined): node is ImageNode {
|
||||||
if (!node) return false
|
if (!node) return false
|
||||||
@@ -31,6 +53,13 @@ export function isAudioNode(node: LGraphNode | undefined): boolean {
|
|||||||
return !!node && node.previewMediaType === 'audio'
|
return !!node && node.previewMediaType === 'audio'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isModelNode = (node: unknown): node is ModelNode => {
|
||||||
|
if (!isLGraphNode(node)) return false
|
||||||
|
if (node.properties?.models) return true
|
||||||
|
if (!node.outputs?.length) return false
|
||||||
|
return node.outputs.some((output) => MODEL_OUTPUT_TYPES.has(`${output.type}`))
|
||||||
|
}
|
||||||
|
|
||||||
export function addToComboValues(widget: IComboWidget, value: string) {
|
export function addToComboValues(widget: IComboWidget, value: string) {
|
||||||
if (!widget.options) widget.options = { values: [] }
|
if (!widget.options) widget.options = { values: [] }
|
||||||
if (!widget.options.values) widget.options.values = []
|
if (!widget.options.values) widget.options.values = []
|
||||||
@@ -46,7 +75,6 @@ export const isLGraphNode = (item: unknown): item is LGraphNode => {
|
|||||||
export const isLGraphGroup = (item: unknown): item is LGraphGroup => {
|
export const isLGraphGroup = (item: unknown): item is LGraphGroup => {
|
||||||
return item instanceof LGraphGroup
|
return item instanceof LGraphGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the color option of all canvas items if they are all the same.
|
* Get the color option of all canvas items if they are all the same.
|
||||||
* @param items - The items to get the color option of.
|
* @param items - The items to get the color option of.
|
||||||
|
|||||||
Reference in New Issue
Block a user