refactor: improve Vue widget type safety and runtime prop handling

- Add proper type guards for all widget input specs (Color, TreeSelect, MultiSelect, FileUpload, Galleria)
- Enhance schemas with missing properties (format, placeholder, accept, extensions, tooltip)
- Fix widgets to honor runtime props like disabled while accessing spec metadata
- Eliminate all 'as any' usage in widget components with proper TypeScript types
- Clean separation: widget.spec.options for metadata, widget.options for runtime state
- Refactor devtools into modular structure with vue_widgets showcase nodes
This commit is contained in:
bymyself
2025-11-06 13:31:27 -07:00
parent e0e3612588
commit 7cfa213fc8
12 changed files with 699 additions and 41 deletions

View File

@@ -0,0 +1,41 @@
import fs from 'fs-extra'
import path from 'path'
import { fileURLToPath } from 'url'
export function syncDevtools(targetComfyDir: string) {
if (!targetComfyDir) {
console.warn('syncDevtools skipped: TEST_COMFYUI_DIR not set')
return
}
const moduleDir =
typeof __dirname !== 'undefined'
? __dirname
: path.dirname(fileURLToPath(import.meta.url))
const devtoolsSrc = path.resolve(moduleDir, '..', '..', 'tools', 'devtools')
if (!fs.pathExistsSync(devtoolsSrc)) {
console.warn(
`syncDevtools skipped: source directory not found at ${devtoolsSrc}`
)
return
}
const devtoolsDest = path.resolve(
targetComfyDir,
'custom_nodes',
'ComfyUI_devtools'
)
console.warn(`syncDevtools: copying ${devtoolsSrc} -> ${devtoolsDest}`)
try {
fs.removeSync(devtoolsDest)
fs.ensureDirSync(devtoolsDest)
fs.copySync(devtoolsSrc, devtoolsDest, { overwrite: true })
console.warn('syncDevtools: copy complete')
} catch (error) {
console.error(`Failed to sync DevTools to ${devtoolsDest}:`, error)
}
}

View File

@@ -27,6 +27,7 @@
import ColorPicker from 'primevue/colorpicker'
import { computed, ref, watch } from 'vue'
import { isColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { isColorFormat, toHexFromFormat } from '@/utils/colorUtil'
import type { ColorFormat, HSB } from '@/utils/colorUtil'
@@ -51,18 +52,18 @@ const emit = defineEmits<{
}>()
const format = computed<ColorFormat>(() => {
const optionFormat = props.widget.options?.format
const spec = props.widget.spec
if (!spec || !isColorInputSpec(spec)) {
return 'hex'
}
const optionFormat = spec.options?.format
return isColorFormat(optionFormat) ? optionFormat : 'hex'
})
type PickerValue = string | HSB
const localValue = ref<PickerValue>(
toHexFromFormat(
props.modelValue || '#000000',
isColorFormat(props.widget.options?.format)
? props.widget.options.format
: 'hex'
)
toHexFromFormat(props.modelValue || '#000000', format.value)
)
watch(

View File

@@ -176,7 +176,11 @@
ref="fileInputRef"
type="file"
class="hidden"
:accept="widget.options?.accept"
:accept="
widget.spec && isFileUploadInputSpec(widget.spec)
? widget.spec.options?.accept
: undefined
"
:aria-label="`${$t('g.upload')} ${widget.name || $t('g.file')}`"
:multiple="false"
@change="handleFileChange"
@@ -190,6 +194,7 @@ import { computed, onUnmounted, ref, watch } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
import { isFileUploadInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
const { widget, modelValue } = defineProps<{

View File

@@ -53,6 +53,7 @@ import Galleria from 'primevue/galleria'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { isGalleriaInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
GALLERIA_EXCLUDED_PROPS,
@@ -78,9 +79,9 @@ const activeIndex = ref(0)
const { t } = useI18n()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, GALLERIA_EXCLUDED_PROPS)
)
const filteredProps = computed(() => {
return filterWidgetProps(props.widget.options, GALLERIA_EXCLUDED_PROPS)
})
const galleryImages = computed(() => {
if (!value.value || !Array.isArray(value.value)) return []
@@ -100,16 +101,22 @@ const galleryImages = computed(() => {
})
const showThumbnails = computed(() => {
const spec = props.widget.spec
if (!spec || !isGalleriaInputSpec(spec)) {
return galleryImages.value.length > 1
}
return (
props.widget.options?.showThumbnails !== false &&
galleryImages.value.length > 1
spec.options?.showThumbnails !== false && galleryImages.value.length > 1
)
})
const showNavButtons = computed(() => {
const spec = props.widget.spec
if (!spec || !isGalleriaInputSpec(spec)) {
return galleryImages.value.length > 1
}
return (
props.widget.options?.showItemNavigators !== false &&
galleryImages.value.length > 1
spec.options?.showItemNavigators !== false && galleryImages.value.length > 1
)
})
</script>

View File

@@ -23,6 +23,7 @@ import { computed } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
import { isMultiSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
import {
PANEL_EXCLUDED_PROPS,
@@ -57,19 +58,20 @@ const MULTISELECT_EXCLUDED_PROPS = [
'overlayStyle'
] as const
// Extract spec options directly
const combinedProps = computed(() => ({
...filterWidgetProps(props.widget.options, MULTISELECT_EXCLUDED_PROPS),
...transformCompatProps.value
}))
// Extract multiselect options from widget options
// Extract multiselect options from widget spec options
const multiSelectOptions = computed((): T[] => {
const options = props.widget.options
if (Array.isArray(options?.values)) {
return options.values
const spec = props.widget.spec
if (!spec || !isMultiSelectInputSpec(spec)) {
return []
}
return []
const values = spec.options?.values
return Array.isArray(values) ? (values as T[]) : []
})
</script>

View File

@@ -2,7 +2,7 @@
<WidgetLayoutField :widget="widget">
<FormSelectButton
v-model="localValue"
:options="widget.options?.values || []"
:options="selectOptions"
class="w-full"
@update:model-value="onChange"
/>
@@ -10,7 +10,10 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useStringWidgetValue } from '@/composables/graph/useWidgetValue'
import { isSelectButtonInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import FormSelectButton from './form/FormSelectButton.vue'
@@ -31,4 +34,13 @@ const { localValue, onChange } = useStringWidgetValue(
props.modelValue,
emit
)
// Extract spec options directly
const selectOptions = computed(() => {
const spec = props.widget.spec
if (!spec || !isSelectButtonInputSpec(spec)) {
return []
}
return spec.options?.values || []
})
</script>

View File

@@ -20,11 +20,8 @@ import { computed } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
import { isTreeSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
PANEL_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
@@ -57,15 +54,29 @@ const { localValue, onChange } = useWidgetValue({
// Transform compatibility props for overlay positioning
const transformCompatProps = useTransformCompatOverlayProps()
// TreeSelect specific excluded props
const TREE_SELECT_EXCLUDED_PROPS = [
...PANEL_EXCLUDED_PROPS,
'inputClass',
'inputStyle'
] as const
const combinedProps = computed(() => {
const spec = props.widget.spec
if (!spec || !isTreeSelectInputSpec(spec)) {
return {
...props.widget.options,
...transformCompatProps.value
}
}
const combinedProps = computed(() => ({
...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS),
...transformCompatProps.value
}))
const specOptions = spec.options || {}
return {
// Include runtime props like disabled
...props.widget.options,
// PrimeVue TreeSelect expects 'options' to be an array of tree nodes
options: (specOptions.values as TreeNode[]) || [],
// Convert 'multiple' to PrimeVue's 'selectionMode'
selectionMode: (specOptions.multiple ? 'multiple' : 'single') as
| 'single'
| 'multiple'
| 'checkbox',
// Pass through other props like placeholder
placeholder: specOptions.placeholder,
...transformCompatProps.value
}
})
</script>

View File

@@ -45,7 +45,8 @@ const zColorInputSpec = zBaseInputOptions.extend({
isOptional: z.boolean().optional(),
options: z
.object({
default: z.string().optional()
default: z.string().optional(),
format: z.enum(['hex', 'rgb', 'hsl', 'hsb']).optional()
})
.optional()
})
@@ -54,7 +55,13 @@ const zFileUploadInputSpec = zBaseInputOptions.extend({
type: z.literal('FILEUPLOAD'),
name: z.string(),
isOptional: z.boolean().optional(),
options: z.record(z.unknown()).optional()
options: z
.object({
accept: z.string().optional(),
extensions: z.array(z.string()).optional(),
tooltip: z.string().optional()
})
.optional()
})
const zImageInputSpec = zBaseInputOptions.extend({
@@ -89,7 +96,8 @@ const zTreeSelectInputSpec = zBaseInputOptions.extend({
options: z
.object({
multiple: z.boolean().optional(),
values: z.array(z.unknown()).optional()
values: z.array(z.unknown()).optional(),
placeholder: z.string().optional()
})
.optional()
})
@@ -123,7 +131,9 @@ const zGalleriaInputSpec = zBaseInputOptions.extend({
isOptional: z.boolean().optional(),
options: z
.object({
images: z.array(z.string()).optional()
images: z.array(z.string()).optional(),
showThumbnails: z.boolean().optional(),
showItemNavigators: z.boolean().optional()
})
.optional()
})
@@ -262,3 +272,39 @@ export const isChartInputSpec = (
): inputSpec is ChartInputSpec => {
return inputSpec.type === 'CHART'
}
export const isTreeSelectInputSpec = (
inputSpec: InputSpec
): inputSpec is TreeSelectInputSpec => {
return inputSpec.type === 'TREESELECT'
}
export const isSelectButtonInputSpec = (
inputSpec: InputSpec
): inputSpec is SelectButtonInputSpec => {
return inputSpec.type === 'SELECTBUTTON'
}
export const isMultiSelectInputSpec = (
inputSpec: InputSpec
): inputSpec is MultiSelectInputSpec => {
return inputSpec.type === 'MULTISELECT'
}
export const isGalleriaInputSpec = (
inputSpec: InputSpec
): inputSpec is GalleriaInputSpec => {
return inputSpec.type === 'GALLERIA'
}
export const isColorInputSpec = (
inputSpec: InputSpec
): inputSpec is ColorInputSpec => {
return inputSpec.type === 'COLOR'
}
export const isFileUploadInputSpec = (
inputSpec: InputSpec
): inputSpec is FileUploadInputSpec => {
return inputSpec.type === 'FILEUPLOAD'
}

View File

@@ -6,11 +6,13 @@ This directory contains development tools and test utilities for ComfyUI, previo
- `__init__.py` - Server endpoints for development tools (`/api/devtools/*`)
- `dev_nodes.py` - Development and testing nodes for ComfyUI
- `nodes/vue_widgets.py` - Widget showcase nodes used to exercise new Vue-based widgets
- `fake_model.safetensors` - Test fixture for model loading tests
## Purpose
These tools provide:
- Test endpoints for browser automation
- Development nodes for testing various UI features
- Mock data for consistent testing environments
@@ -25,4 +27,4 @@ cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
## Migration
This directory was created as part of issue #4683 to merge the ComfyUI_devtools repository into the main frontend repository, eliminating the need for separate versioning and simplifying the development workflow.
This directory was created as part of issue #4683 to merge the ComfyUI_devtools repository into the main frontend repository, eliminating the need for separate versioning and simplifying the development workflow.

View File

@@ -1,13 +1,20 @@
from __future__ import annotations
from .nodes import (
VueAudioPreviewComboNode,
VueAudioRecordWidgetNode,
VueChartWidgetNode,
DeprecatedNode,
DummyPatch,
ErrorRaiseNode,
ErrorRaiseNodeWithMessage,
ExperimentalNode,
VueFileUploadWidgetNode,
LoadAnimatedImageTest,
LongComboDropdown,
VueMarkdownWidgetNode,
VueGalleriaWidgetNode,
VueImageCompareWidgetNode,
MultiSelectNode,
NodeWithBooleanInput,
NodeWithDefaultInput,
@@ -23,24 +30,36 @@ from .nodes import (
NodeWithValidation,
NodeWithV2ComboInput,
ObjectPatchNode,
VueSelectButtonWidgetNode,
VueTextareaWidgetNode,
VueTreeSelectMultiWidgetNode,
VueTreeSelectWidgetNode,
RemoteWidgetNode,
RemoteWidgetNodeWithControlAfterRefresh,
RemoteWidgetNodeWithParams,
RemoteWidgetNodeWithRefresh,
RemoteWidgetNodeWithRefreshButton,
SimpleSlider,
VueColorWidgetNode,
NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS,
)
__all__ = [
"VueAudioPreviewComboNode",
"VueAudioRecordWidgetNode",
"VueChartWidgetNode",
"DeprecatedNode",
"DummyPatch",
"ErrorRaiseNode",
"ErrorRaiseNodeWithMessage",
"ExperimentalNode",
"VueFileUploadWidgetNode",
"LoadAnimatedImageTest",
"LongComboDropdown",
"VueMarkdownWidgetNode",
"VueGalleriaWidgetNode",
"VueImageCompareWidgetNode",
"MultiSelectNode",
"NodeWithBooleanInput",
"NodeWithDefaultInput",
@@ -56,12 +75,17 @@ __all__ = [
"NodeWithValidation",
"NodeWithV2ComboInput",
"ObjectPatchNode",
"VueSelectButtonWidgetNode",
"VueTextareaWidgetNode",
"VueTreeSelectMultiWidgetNode",
"VueTreeSelectWidgetNode",
"RemoteWidgetNode",
"RemoteWidgetNodeWithControlAfterRefresh",
"RemoteWidgetNodeWithParams",
"RemoteWidgetNodeWithRefresh",
"RemoteWidgetNodeWithRefreshButton",
"SimpleSlider",
"VueColorWidgetNode",
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
]

View File

@@ -44,12 +44,29 @@ from .remote import (
NODE_CLASS_MAPPINGS as remote_class_mappings,
NODE_DISPLAY_NAME_MAPPINGS as remote_display_name_mappings,
)
from .vue_widgets import (
VueAudioPreviewComboNode,
VueAudioRecordWidgetNode,
VueChartWidgetNode,
VueColorWidgetNode,
VueFileUploadWidgetNode,
VueGalleriaWidgetNode,
VueImageCompareWidgetNode,
VueMarkdownWidgetNode,
VueSelectButtonWidgetNode,
VueTextareaWidgetNode,
VueTreeSelectMultiWidgetNode,
VueTreeSelectWidgetNode,
NODE_CLASS_MAPPINGS as vue_widgets_class_mappings,
NODE_DISPLAY_NAME_MAPPINGS as vue_widgets_display_name_mappings,
)
NODE_CLASS_MAPPINGS = {
**errors_class_mappings,
**inputs_class_mappings,
**remote_class_mappings,
**models_class_mappings,
**vue_widgets_class_mappings,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -57,6 +74,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
**inputs_display_name_mappings,
**remote_display_name_mappings,
**models_display_name_mappings,
**vue_widgets_display_name_mappings,
}
__all__ = [
@@ -88,6 +106,18 @@ __all__ = [
"RemoteWidgetNodeWithRefresh",
"RemoteWidgetNodeWithRefreshButton",
"SimpleSlider",
"VueAudioPreviewComboNode",
"VueAudioRecordWidgetNode",
"VueChartWidgetNode",
"VueColorWidgetNode",
"VueFileUploadWidgetNode",
"VueGalleriaWidgetNode",
"VueImageCompareWidgetNode",
"VueMarkdownWidgetNode",
"VueSelectButtonWidgetNode",
"VueTextareaWidgetNode",
"VueTreeSelectMultiWidgetNode",
"VueTreeSelectWidgetNode",
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
]

View File

@@ -0,0 +1,477 @@
from __future__ import annotations
SAMPLE_IMAGE_DATA_URI = (
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKklEQVR4nO3NsQEAAATAMLzr/5kn2NIDmpzu+Kxe7wAAAAAAAAAAAOCwBcUDAhU8Tp3xAAAAAElFTkSuQmCC"
)
SAMPLE_IMAGE_DATA_URI_ALT = (
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAALUlEQVR4nGM0nvWBgZaAiaamj1owasGoBaMWjFowasGoBaMWjFowasGoBVQEAKDTAf3D6Eg+AAAAAElFTkSuQmCC"
)
SAMPLE_IMAGE_DATA_URI_THIRD = (
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAALUlEQVR4nGMMPJ/GQEvARFPTRy0YtWDUglELRi0YtWDUglELRi0YtWDUAioCAAbrAcZ7cQHKAAAAAElFTkSuQmCC"
)
class VueFileUploadWidgetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"uploaded_file": (
"FILEUPLOAD",
{
"default": [],
"options": {
"extensions": ["png", "jpg", "jpeg", "webp"],
"accept": "image/png,image/jpeg,image/webp",
"tooltip": "Upload an image file",
},
},
)
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "return_file"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the FILEUPLOAD widget"
def return_file(self, uploaded_file: str | None):
return (uploaded_file or "",)
class VueImageCompareWidgetNode:
BEFORE_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAB4CAIAAAA48Cq8AAAC8ElEQVR4nO3Wv0uqURzH8Y/ykNXgFBQ1NERBNIS7jTUF4VBQ7RE4SH9Hg0jQ3KA1WVO74RYWREskREE0NRUNYvrc4XZBL3V/wP3kNd+v7eGcL5zhzTlPJEynBfxr0U4fAF8TYcGCsGBBWLAgLFgQFiwICxaEBQvCggVhwYKwYEFYsCAsWBAWLAgLFoQFC8KCBWHBgrBgQViwICxYEBYsCAsWhAULwoIFYcGCsGBBWLAgLFgQFiwICxaEBQvCggVhwYKwYEFYsCAsWBAWLAgLFoQFC8KCBWHBgrBgQViwICxYEBYsuhHW6KgWFzU01IWj8b8EEf9ePq/7e8ViGhzU6amurz/Zs7WlalVPNbMdHsJdEHdbzu07aaY0Pb252Elk6pUIj4XPSbqsH55elKzqeFhra4qmVQQ6PhYDw9aWFAioVxOpZLW1pRIKAxVLOrlRbu7urrS46POz3+fQr+xfWPNzKhcViajszPt7engQBsbklStKgxVKGh5WTVa8nnVaspiLCtCh8e6m0N7NZpclMUyo3LUqk0U0Ppra+vwOaPNZCn1ddn8dEtYvVSpOYqSRMT89g4MdKRpJKpRMoqXTdfqfBWm08RF4z/y9Ktfm+fXpLLjEgcFPTd1WsHyJRKxBqVTpIqH9f1f3xBd5JJkEplI+LtMv4ucVaFZ7o2j/AuAxn4OwIEFYkCAsSBAWJAgLEoQFCcKCBGFBgrAgQViQICxIEBYkCAsShAUJwoIEYUGCsCBBWJAgLEgQFiQICxKEBQnCggRhQYKwIEFYkCAsSBAWJAgLEoQFCcKCBGFBgrAgQViQICxIEBYkCAsShAUJwoIEYUGCsCBBWJAgLEgQFiw+AcwZf7lBBNZAAA"
AFTER_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAB4CAIAAAA48Cq8AAADDklEQVR4nO3Z32uNcRzA8e/3Oc85Oz/WLFqNzjZuxKgtWpmwluxiUYoLudlKubH/QHJBuZE7LuYfoCQXolxJasmF1EjJhRrhQpQ1O85zjouzEm69d7Ler6vvU8+Pz8W7b9964u7ZL0H615J2D6C1ybCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIRY1bA2dydTQx2dhbiaH1VbsGEd3VaYO7VufWmlpMuHKstZM2uE6eEO9LtqOzasA/3pjfnlfX351uWGcrwxX1uqN6eHDGuNA8MqprGYxjuvavsG0hDC8cFCJR9nD1dmRorlfLw6WenqiBfGy9cmK9ePVHb05FpPPZzqOj9WOrGzwA2mVZByrx6tpnML9bdfG5s6k3wSbr2szYwUT99dDCEcHyycubd47kDp5ovl+U9Zb2dyZaJ88va3EEIhFx+8+TG3UOcG0yoAwxobSLduyB3cku8pJ7s2pk/e/dnKaDXt61rZMkv5mMTQaIas2fz7Tv13qLCSGPrX5Vqb0Gg13d+f/zuXXIwz9xdrWUhiGE5NG80QQsgaobXQf406Yw33pq8/Z631sw/ZnupvBScxJDE8/1gf35wPIeztSz3OrzHUjjU2kD59v7JFfa83Py81tnT/ivjZh+zKROXS46Wz+0vHtheyZrj4aAmaRG0Rd89+afcMWoP8pSOEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQvwE4Ex5XANtu7QAAAAASUVORK5CYII="
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"comparison": (
"IMAGECOMPARE",
{
"default": {
"before": BEFORE_IMAGE,
"after": AFTER_IMAGE,
},
"options": {
"beforeAlt": "Before",
"afterAlt": "After",
"initialPosition": 40,
},
},
)
}
}
RETURN_TYPES = ()
FUNCTION = "noop"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the IMAGECOMPARE widget"
def noop(self, comparison):
return tuple()
class VueTreeSelectWidgetNode:
TREE_DATA = [
{
"key": "root",
"label": "Root",
"children": [
{
"key": "section-a",
"label": "Section A",
"children": [
{"key": "item-a1", "label": "Item A1"},
{"key": "item-a2", "label": "Item A2"},
],
},
{
"key": "section-b",
"label": "Section B",
"children": [
{"key": "item-b1", "label": "Item B1"},
{"key": "item-b2", "label": "Item B2"},
],
},
],
}
]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"selection": (
"TREESELECT",
{
"default": "item-a1",
"options": {
"values": cls.TREE_DATA,
"multiple": False,
"placeholder": "Select an item",
},
},
)
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "return_selection"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the TREESELECT widget"
def return_selection(self, selection: str):
return (selection,)
class VueTreeSelectMultiWidgetNode(VueTreeSelectWidgetNode):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"selection": (
"TREESELECT",
{
"default": ["item-a1", "item-b1"],
"options": {
"values": cls.TREE_DATA,
"multiple": True,
"placeholder": "Select items",
},
},
)
}
}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "return_selection"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the TREESELECT widget in multi-select mode"
def return_selection(self, selection: list[str]):
return (selection,)
class VueSelectButtonWidgetNode:
@classmethod
def INPUT_TYPES(cls):
options = [
{"label": "Low", "value": "low"},
{"label": "Medium", "value": "medium"},
{"label": "High", "value": "high"},
]
return {
"required": {
"mode": (
"SELECTBUTTON",
{
"default": "Medium",
"options": {
"values": ["Low", "Medium", "High"],
},
},
)
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "return_mode"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the SELECTBUTTON widget"
def return_mode(self, mode: str):
return (mode,)
class VueTextareaWidgetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"notes": (
"TEXTAREA",
{
"default": "This is a DevTools textarea widget.\nFeel free to edit me!",
"options": {
"rows": 4,
"cols": 40,
},
},
)
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "return_notes"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the TEXTAREA widget"
def return_notes(self, notes: str):
return (notes,)
class VueChartWidgetNode:
CHART_DATA = {
"labels": ["Iteration 1", "Iteration 2", "Iteration 3"],
"datasets": [
{
"label": "Loss",
"data": [0.95, 0.62, 0.31],
"borderColor": "#339AF0",
"backgroundColor": "rgba(51, 154, 240, 0.2)",
"fill": True,
"tension": 0.35,
}
],
}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"chart": (
"CHART",
{
"options": {
"type": "line",
"data": cls.CHART_DATA,
},
},
)
}
}
RETURN_TYPES = ("DICT",)
FUNCTION = "return_chart"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the CHART widget"
def return_chart(self, chart):
return (chart,)
class VueGalleriaWidgetNode:
GALLERIA_IMAGES = [
{
"itemImageSrc": SAMPLE_IMAGE_DATA_URI,
"thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI,
"alt": "Warm gradient",
},
{
"itemImageSrc": SAMPLE_IMAGE_DATA_URI_ALT,
"thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI_ALT,
"alt": "Cool gradient",
},
{
"itemImageSrc": SAMPLE_IMAGE_DATA_URI_THIRD,
"thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI_THIRD,
"alt": "Fresh gradient",
},
]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"gallery": (
"GALLERIA",
{
"default": cls.GALLERIA_IMAGES,
"options": {
"images": cls.GALLERIA_IMAGES,
"showThumbnails": True,
"showItemNavigators": True,
},
},
)
}
}
RETURN_TYPES = ()
FUNCTION = "noop"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the GALLERIA widget"
def noop(self, gallery):
return tuple()
class VueMarkdownWidgetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"markdown": (
"MARKDOWN",
{
"default": "# DevTools Markdown\nThis widget renders **Markdown** content.",
"options": {
"content": "# DevTools Markdown\nThis widget renders **Markdown** content.",
},
},
)
}
}
RETURN_TYPES = ()
FUNCTION = "noop"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the MARKDOWN widget"
def noop(self, markdown):
return tuple()
class VueAudioRecordWidgetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"recording": (
"AUDIORECORD",
{
"default": "",
},
)
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "return_recording"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the AUDIORECORD widget"
def return_recording(self, recording: str):
return (recording,)
class VueMultiSelectWidgetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"selection": (
"MULTISELECT",
{
"default": ["option1", "option3"],
"options": {
"values": ["option1", "option2", "option3", "option4", "option5"],
},
},
)
}
}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "return_selection"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the MULTISELECT widget"
def return_selection(self, selection: list[str]):
return (selection,)
class VueColorWidgetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"color": (
"COLOR",
{
"default": "#ff6b6b",
"options": {
"tooltip": "Pick a color",
},
},
)
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "return_color"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the COLOR widget"
def return_color(self, color: str):
return (color,)
class VueAudioPreviewComboNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio": (
"COMBO",
{
"options": ["ambient.wav", "dialog.wav"],
"default": "ambient.wav",
"tooltip": "Pick an audio clip",
},
)
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "return_audio"
CATEGORY = "DevTools/Vue Widgets"
DESCRIPTION = "Showcases the COMBO widget rendered as Audio UI"
def return_audio(self, audio: str):
return (audio,)
NODE_CLASS_MAPPINGS = {
"DevToolsVueFileUploadWidgetNode": VueFileUploadWidgetNode,
"DevToolsVueImageCompareWidgetNode": VueImageCompareWidgetNode,
"DevToolsVueTreeSelectWidgetNode": VueTreeSelectWidgetNode,
"DevToolsVueTreeSelectMultiWidgetNode": VueTreeSelectMultiWidgetNode,
"DevToolsVueSelectButtonWidgetNode": VueSelectButtonWidgetNode,
"DevToolsVueMultiSelectWidgetNode": VueMultiSelectWidgetNode,
"DevToolsVueTextareaWidgetNode": VueTextareaWidgetNode,
"DevToolsVueChartWidgetNode": VueChartWidgetNode,
"DevToolsVueGalleriaWidgetNode": VueGalleriaWidgetNode,
"DevToolsVueMarkdownWidgetNode": VueMarkdownWidgetNode,
"DevToolsVueAudioRecordWidgetNode": VueAudioRecordWidgetNode,
"DevToolsVueColorWidgetNode": VueColorWidgetNode,
"DevToolsVueAudioPreviewComboNode": VueAudioPreviewComboNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DevToolsVueFileUploadWidgetNode": "Vue File Upload Widget",
"DevToolsVueImageCompareWidgetNode": "Vue Image Compare Widget",
"DevToolsVueTreeSelectWidgetNode": "Vue Tree Select Widget",
"DevToolsVueTreeSelectMultiWidgetNode": "Vue Tree Select (Multi) Widget",
"DevToolsVueSelectButtonWidgetNode": "Vue Select Button Widget",
"DevToolsVueMultiSelectWidgetNode": "Vue Multi Select Widget",
"DevToolsVueTextareaWidgetNode": "Vue Textarea Widget",
"DevToolsVueChartWidgetNode": "Vue Chart Widget",
"DevToolsVueGalleriaWidgetNode": "Vue Galleria Widget",
"DevToolsVueMarkdownWidgetNode": "Vue Markdown Widget",
"DevToolsVueAudioRecordWidgetNode": "Vue Audio Record Widget",
"DevToolsVueColorWidgetNode": "Vue Color Widget",
"DevToolsVueAudioPreviewComboNode": "Vue Audio Combo Widget",
}
__all__ = [
"VueFileUploadWidgetNode",
"VueImageCompareWidgetNode",
"VueTreeSelectWidgetNode",
"VueTreeSelectMultiWidgetNode",
"VueSelectButtonWidgetNode",
"VueMultiSelectWidgetNode",
"VueTextareaWidgetNode",
"VueChartWidgetNode",
"VueGalleriaWidgetNode",
"VueMarkdownWidgetNode",
"VueAudioRecordWidgetNode",
"VueColorWidgetNode",
"VueAudioPreviewComboNode",
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
]