diff --git a/browser_tests/utils/devtoolsSync.ts b/browser_tests/utils/devtoolsSync.ts new file mode 100644 index 000000000..594effe7d --- /dev/null +++ b/browser_tests/utils/devtoolsSync.ts @@ -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) + } +} diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue index 333966996..85396754c 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue @@ -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(() => { - 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( - toHexFromFormat( - props.modelValue || '#000000', - isColorFormat(props.widget.options?.format) - ? props.widget.options.format - : 'hex' - ) + toHexFromFormat(props.modelValue || '#000000', format.value) ) watch( diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue index adef141bf..c907613d0 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue @@ -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<{ diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue index e32acb781..23809431b 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue @@ -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 ) }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue index 167d45c2c..ef01a89e0 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue @@ -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[]) : [] }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue index 9a5663957..bed7d246a 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue @@ -2,7 +2,7 @@ @@ -10,7 +10,10 @@ diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue index fae52b530..f64a4c258 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue @@ -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 + } +}) diff --git a/src/schemas/nodeDef/nodeDefSchemaV2.ts b/src/schemas/nodeDef/nodeDefSchemaV2.ts index 09983d115..4ffd540fc 100644 --- a/src/schemas/nodeDef/nodeDefSchemaV2.ts +++ b/src/schemas/nodeDef/nodeDefSchemaV2.ts @@ -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' +} diff --git a/tools/devtools/README.md b/tools/devtools/README.md index d0d316ce8..dd6da43b5 100644 --- a/tools/devtools/README.md +++ b/tools/devtools/README.md @@ -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. \ No newline at end of file +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. diff --git a/tools/devtools/dev_nodes.py b/tools/devtools/dev_nodes.py index 660518d84..c8bf860a1 100644 --- a/tools/devtools/dev_nodes.py +++ b/tools/devtools/dev_nodes.py @@ -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", ] diff --git a/tools/devtools/nodes/__init__.py b/tools/devtools/nodes/__init__.py index f0ac2d8ee..3be4f5f99 100644 --- a/tools/devtools/nodes/__init__.py +++ b/tools/devtools/nodes/__init__.py @@ -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", ] diff --git a/tools/devtools/nodes/vue_widgets.py b/tools/devtools/nodes/vue_widgets.py new file mode 100644 index 000000000..16fc48edb --- /dev/null +++ b/tools/devtools/nodes/vue_widgets.py @@ -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", +]