Compare commits

...

25 Commits

Author SHA1 Message Date
Austin Mroz
867c50f7cb Prune dead code, tighten interval, add screenshot
Code related to possible options has been pruned. It was pointless for
updateNode to take a node as input. Collapsing documentation items does
not make sense given the greater space of a sidebar tab compared to a
floating window on the graph, so the corresponding code has been fully
pruned as well.

The topmost node is used instead of the current_node. While current_node
displayed the desired properties when canvas was still shallowReactive
of notifying a change in node, it's not intended to be used external to
litegraph and at best, tracks the topmost visible node instead of the
actual topmost node.

A test expectation screenshot has been added for verifying theming works
for the documentation tab.
2024-11-28 10:45:23 -06:00
Austin Mroz
89027ea969 Update tests 2024-11-28 02:43:25 -06:00
Austin Mroz
ce9cfdb975 Check for swapped node on interval.
Not ideal, but implementation is low cost and ensures the displayed
documentation properly updates.
2024-11-28 02:23:50 -06:00
Austin Mroz
ef191033c8 Merge sidebar-documentation onto main
The sidebar-documentation branch has diverged enough from main to
merging non-trivial

Remove deprecated type usage.
Move localization to language file.
2024-11-27 18:04:36 -06:00
Austin Mroz
293f4295a8 Add tests for advanced description formats 2024-11-15 15:34:14 -06:00
Austin Mroz
3252d62edf Fix missing await 2024-10-18 21:46:10 -05:00
Austin Mroz
1dfcc7a0d4 Migrate tooltip tracking to a pinia store
While I was concerned that doing this would remove the capability to
suppress tooltips on the active node, clearing the hoveredItem when it
used for documentation functions without even producing a temporary
tooltip.

A future commit will likely be made so that disabling tooltips for nodes
doesn't also prevent the hovered item from being tracked in the store.
2024-10-18 18:56:53 -05:00
Austin Mroz
f48594fbd5 Properly mirror the new description type
Remove errant logging
2024-10-18 15:53:04 -05:00
Austin Mroz
9263330379 Update tests for vue port
Mostly minor changes to selectors

Also fixes the glaringly obvious omission of the description field
2024-10-15 13:41:59 -05:00
Austin Mroz
214f48a6c4 Functional node swaps under vue, icon update 2024-10-15 13:41:59 -05:00
Austin Mroz
f8ba0ab24f Migrate to new sidebar registration 2024-10-15 13:41:59 -05:00
Austin Mroz
b2ef66e058 Update tooltip handling 2024-10-15 13:41:58 -05:00
Austin Mroz
95a4fe7e08 Port sidebar documentation to vue component 2024-10-15 13:41:58 -05:00
Austin Mroz
95cec85c3f Move css to style.css
Since the the css is now static the clutter of an added style element is
no longer needed
2024-10-15 13:41:58 -05:00
Austin Mroz
bc6630742b Move render callback to trigger on node change 2024-10-15 13:41:58 -05:00
Austin Mroz
3b679f1194 Return styling to body, streamline tests
Styling was moved to the sidebar element for better organization, but
this caused errors when the new menu was not in use.
2024-10-15 13:41:58 -05:00
Austin Mroz
52933e13f5 Properly handle theme with css variables 2024-10-15 13:41:58 -05:00
Austin Mroz
44f900ef56 Typing fixes, initial tests 2024-10-15 13:41:58 -05:00
Austin Mroz
7a5d39f41f Temporarily highlight item doc item on selection 2024-10-15 13:41:58 -05:00
Austin Mroz
1d0ae76f8c Connect hover functionality, scroll fixes
Basic connecting for using the existing documentation hover code to
select an item from the active help pane.

Scrolling on selection will now properly perform the minium required
scroll to place the element on screen
2024-10-15 13:41:58 -05:00
Austin Mroz
a8ac7296c2 Theming, pruning, and optional callbacks
Basic styling has been added to the display of documentation for nodes
using the existing tooltip system. This will need another pass to ensure
that style updates immediately when the light/dark toggle is hit instead
of requiring a change of node.

VHS specific namings have been replaced and the code for determining
what the mouse is hovering over has been removed. The existing tooltip
implementation is cleaner and will need to be integrated anyways so
tooltips are temporarily suppressed for the node actively being
displayed in the documentation sidebar.

Optional callbacks have been added for the initial sidebar display and a
user selecting a node element by hovering over it. While selection is
not yet implemented, this should cover any developer needs from more
involved collapsables to automated seeking to video timestamps.
2024-10-15 13:41:58 -05:00
Austin Mroz
4aa04d1419 type implementation for detailed descriptions
Previously, description was a simple string, but supporting more complex
descriptions requires that new data be passed.

The type of a nodes description has been updated to be either a simple
string as before, or an array consisting of short description string, an
html string for the full description, and a placeholder dict for future
usage.

Definitions and usage points for description have been updated to
accommodate this change
2024-10-15 13:41:58 -05:00
Austin Mroz
8160ca0342 Formatting improvements,
The formatting of ndoes using the existing standardized tooltips has
been improved.

Experiemental work for assisting nodes with display of more detailed
descriptions
2024-10-15 13:41:58 -05:00
Austin Mroz
da936d69b6 Remove unused styling code, unwrap 2024-10-15 13:41:58 -05:00
Austin Mroz
7eaa54fe3f Initial sidebar documentation implementation 2024-10-15 13:41:58 -05:00
13 changed files with 418 additions and 16 deletions

View File

@@ -0,0 +1,125 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
const nodeDef = {
title: 'TestNodeAdvancedDoc'
}
test.describe('Documentation Sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
})
test.afterEach(async ({ comfyPage }) => {
const currentThemeId = await comfyPage.menu.getThemeId()
if (currentThemeId !== 'dark') {
await comfyPage.menu.toggleTheme()
}
})
test('Sidebar registered', async ({ comfyPage }) => {
await expect(
comfyPage.page.locator('.documentation-tab-button')
).toBeVisible()
})
test('Parses help for basic node', async ({ comfyPage }) => {
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container')
//Check that each independently parsed element exists
await expect(docPane).toContainText('Load Checkpoint')
await expect(docPane).toContainText('Loads a diffusion model')
await expect(docPane).toContainText('The name of the checkpoint')
await expect(docPane).toContainText('The VAE model used')
})
test('Responds to hovering over node', async ({ comfyPage }) => {
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container')
await comfyPage.page.mouse.move(321, 593)
const tooltipTimeout = 500
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
await expect(comfyPage.page.locator('.node-tooltip')).not.toBeVisible()
await expect(
comfyPage.page.locator('.sidebar-content-container>div>div:nth-child(4)')
).toBeFocused()
})
test('Updates when a new node is selected', async ({ comfyPage }) => {
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container')
await comfyPage.page.mouse.click(557, 440)
await expect(docPane).not.toContainText('Load Checkpoint')
await expect(docPane).toContainText('CLIP Text Encode (Prompt)')
await expect(docPane).toContainText('The text to be encoded')
await expect(docPane).toContainText(
'A conditioning containing the embedded text'
)
})
test('Responds to a change in theme', async ({ comfyPage }) => {
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container')
await comfyPage.menu.toggleTheme()
await expect(docPane).toHaveScreenshot(
'documentation-sidebar-light-theme.png'
)
})
})
test.describe('Advanced Description tests', () => {
test.beforeEach(async ({ comfyPage }) => {
//register test node and add to graph
await comfyPage.page.evaluate(async (node) => {
const app = window['app']
await app.registerNodeDef(node.name, node)
app.addNodeOnGraph(node)
}, advDocNode)
})
test('Description displays as raw html', async ({ comfyPage }) => {
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container>div')
await expect(docPane).toHaveJSProperty(
'innerHTML',
advDocNode.description[1]
)
})
test('selected function', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const app = window['app']
const desc =
LiteGraph.registered_node_types['Test_AdvancedDescription'].nodeData
.description
desc[2].select = (element, name, value) => {
element.children[0].innerText = name + ' ' + value
}
})
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container')
await comfyPage.page.mouse.move(307, 80)
const tooltipTimeout = 500
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
await expect(comfyPage.page.locator('.node-tooltip')).not.toBeVisible()
await expect(docPane).toContainText('int_input 0')
})
})
const advDocNode = {
display_name: 'Node With Advanced Description',
name: 'Test_AdvancedDescription',
input: {
required: {
int_input: [
'INT',
{ tooltip: "an input tooltip that won't be displayed in sidebar" }
]
}
},
output: ['INT'],
output_name: ['int_output'],
output_tooltips: ["An output tooltip that won't be displayed in the sidebar"],
output_is_list: false,
description: [
'A node with description in the advanced format',
`
A long form description that will be displayed in the sidebar.
<div doc_title="INT">Can include arbitrary html</div>
<div doc_title="int_input">or out of order widgets</div>
`,
{}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -10,20 +10,23 @@
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { nextTick, ref, watch } from 'vue'
import { LiteGraph } from '@comfyorg/litegraph'
import { app as comfyApp } from '@/scripts/app'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useHoveredItemStore } from '@/stores/graphStore'
import { useEventListener } from '@vueuse/core'
let idleTimeout: number
const nodeDefStore = useNodeDefStore()
const hoveredItemStore = useHoveredItemStore()
const tooltipRef = ref<HTMLDivElement>()
const tooltipText = ref('')
const left = ref<string>()
const top = ref<string>()
const hideTooltip = () => (tooltipText.value = null)
const clearHovered = () => (hoveredItemStore.value = null)
const showTooltip = async (tooltip: string | null | undefined) => {
if (!tooltip) return
@@ -43,6 +46,36 @@ const showTooltip = async (tooltip: string | null | undefined) => {
top.value = comfyApp.canvas.mouse[1] + rect.height + 'px'
}
}
watch(hoveredItemStore, (hoveredItem) => {
if (!hoveredItem.value) {
return hideTooltip()
}
const item = hoveredItem.value
const nodeDef =
nodeDefStore.nodeDefsByName[item.node.type] ??
LiteGraph.registered_node_types[item.node.type]?.nodeData
if (item.type == 'Title') {
let description = nodeDef.description
if (Array.isArray(description)) {
description = description[0]
}
return showTooltip(description)
} else if (item.type == 'Input') {
showTooltip(nodeDef.input.getInput(item.inputName)?.tooltip)
} else if (item.type == 'Output') {
showTooltip(nodeDef?.output?.all?.[item.outputSlot]?.tooltip)
} else if (item.type == 'Widget') {
showTooltip(
item.widget.tooltip ??
(
nodeDef.input.optional?.[item.widget.name] ??
nodeDef.input.required?.[item.widget.name]
)?.tooltip
)
} else {
hideTooltip()
}
})
const onIdle = () => {
const { canvas } = comfyApp
@@ -50,13 +83,15 @@ const onIdle = () => {
if (!node) return
const ctor = node.constructor as { title_mode?: 0 | 1 | 2 | 3 }
const nodeDef = nodeDefStore.nodeDefsByName[node.type]
const nodeDef =
nodeDefStore.nodeDefsByName[node.type] ??
LiteGraph.registered_node_types[node.type]?.nodeData
if (
ctor.title_mode !== LiteGraph.NO_TITLE &&
canvas.graph_mouse[1] < node.pos[1] // If we are over a node, but not within the node then we are on its title
) {
return showTooltip(nodeDef.description)
hoveredItemStore.value = { node, type: 'Title' }
}
if (node.flags?.collapsed) return
@@ -69,7 +104,7 @@ const onIdle = () => {
)
if (inputSlot !== -1) {
const inputName = node.inputs[inputSlot].name
return showTooltip(nodeDef.input.getInput(inputName)?.tooltip)
hoveredItemStore.value = { node, type: 'Input', inputName }
}
const outputSlot = canvas.isOverNodeOutput(
@@ -79,20 +114,18 @@ const onIdle = () => {
[0, 0]
)
if (outputSlot !== -1) {
return showTooltip(nodeDef.output.all?.[outputSlot]?.tooltip)
hoveredItemStore.value = { node, type: 'Output', outputSlot }
}
const widget = comfyApp.canvas.getWidgetAtCursor()
// Dont show for DOM widgets, these use native browser tooltips as we dont get proper mouse events on these
if (widget && !widget.element) {
return showTooltip(
widget.tooltip ?? nodeDef.input.getInput(widget.name)?.tooltip
)
hoveredItemStore.value = { node, type: 'Widget', widget }
}
}
const onMouseMove = (e: MouseEvent) => {
hideTooltip()
clearHovered()
clearTimeout(idleTimeout)
if ((e.target as Node).nodeName !== 'CANVAS') return
@@ -100,7 +133,7 @@ const onMouseMove = (e: MouseEvent) => {
}
useEventListener(window, 'mousemove', onMouseMove)
useEventListener(window, 'click', hideTooltip)
useEventListener(window, 'click', clearHovered)
</script>
<style lang="css" scoped>

View File

@@ -74,7 +74,11 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
backgroundColor: litegraphColors.WIDGET_BGCOLOR
}"
>
{{ nodeDef.description }}
{{
Array.isArray(nodeDef.description)
? nodeDef.description[0]
: nodeDef.description
}}
</div>
</div>
</template>

View File

@@ -0,0 +1,196 @@
<template>
<div v-if="!hasAnyDoc()">Select a node to see documentation.</div>
<div v-else-if="rawDoc" ref="docElement" v-html="rawDoc"></div>
<div v-else ref="docElement">
<div class="doc-node">{{ title }}</div>
<div>{{ description }}</div>
<div v-if="inputs.length" class="doc-section">Inputs</div>
<div
v-if="inputs.length"
v-for="input in inputs"
tabindex="-1"
class="doc-item"
>
{{ input[0] }}
<div>{{ input[1] }}</div>
</div>
<div v-if="outputs.length" class="doc-section">Outputs</div>
<div
v-if="outputs.length"
v-for="output in outputs"
tabindex="-1"
class="doc-item"
>
{{ output[0] }}
<div>{{ output[1] }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onBeforeUnmount, isReactive } from 'vue'
import { app } from '@/scripts/app'
import { useCanvasStore } from '@/stores/graphStore'
import { useHoveredItemStore } from '@/stores/graphStore'
const hoveredItemStore = useHoveredItemStore()
const canvasStore = useCanvasStore()
const docElement = ref(null)
let def
const rawDoc = ref(null)
const description = ref(null)
const title = ref(null)
const inputs = ref([])
const outputs = ref([])
function selectHelp(name: string, value?: string) {
if (!docElement.value || !name) {
return null
}
if (def.description[2]?.select) {
return def.description[2].select(docElement.value, name, value)
}
//attempt to navigate to name in help
function collapseUnlessMatch(items, t) {
var match = items.querySelector('[doc_title="' + t + '"]')
if (!match) {
for (let i of items.children) {
if (i.innerHTML.slice(0, t.length + 5).includes(t)) {
match = i
break
}
}
}
if (!match) {
return null
}
//For longer documentation items with fewer collapsable elements,
//scroll to make sure the entirety of the selected item is visible
match.scrollIntoView({ block: 'nearest' })
//The previous floating help implementation would try to scroll the window
//itself if the display was partiall offscreen. As the sidebar documentation
//does not pan with the canvas, this should no longer be needed
//window.scrollTo(0, 0)
for (let i of items.querySelectorAll('.doc_collapse')) {
if (i.contains(match)) {
setCollapse(i, false)
} else {
setCollapse(i, true)
}
}
return match
}
let target = collapseUnlessMatch(docElement.value, name)
if (target) {
target.focus()
if (value) {
collapseUnlessMatch(target, value)
}
}
}
function updateNode() {
//Grab the topmost node.
//current_node is topmost on screen and
//selectedItems is unordered
const node = app?.graph?._nodes[app?.graph?._nodes.length - 1]
if (!node) {
// Graph has no nodes
return
}
const nodeDef = LiteGraph.getNodeType(node.type).nodeData
if (def == nodeDef) {
return
}
def = nodeDef
title.value = def.display_name
if (Array.isArray(def.description)) {
rawDoc.value = def.description[1]
outputs.value = []
inputs.value = []
return
} else {
rawDoc.value = null
}
description.value = def.description
let input_temp = []
for (let k in def?.input?.required) {
if (def.input.required[k][1]?.tooltip) {
input_temp.push([k, def.input.required[k][1].tooltip])
}
}
for (let k in def?.optional?.required) {
if (def.input.optional[k][1]?.tooltip) {
input_temp.push([k, def.input.optional[k][1].tooltip])
}
}
inputs.value = input_temp
if (def.output_tooltips) {
const outputs_temp = []
const output_name = def.output_name || def.output
for (let i = 0; i < def.output_tooltips.length; i++) {
outputs_temp[i] = [output_name[i], def.output_tooltips[i]]
}
outputs.value = outputs_temp
} else {
outputs.value = []
}
}
function hasAnyDoc() {
return def?.description || inputs.value.length || outputs.value.length
}
watch(hoveredItemStore, (hoveredItem) => {
if (!hoveredItem.value) {
return
}
const item = hoveredItem.value
const nodeDef = LiteGraph.getNodeType(item.node.type).nodeData
if (nodeDef != def) {
return
}
if (item.type == 'DESCRIPTION') {
return
} else if (item.type == 'Input') {
selectHelp(item.inputName)
hoveredItem.value = null
} else if (item.type == 'Output') {
selectHelp(nodeDef?.output?.all?.[item.outputSlot]?.name)
hoveredItem.value = null
} else if (item.type == 'Widget') {
selectHelp(item.widget.name, item.widget.value)
hoveredItem.value = null
}
})
if (isReactive(canvasStore?.canvas)) {
watch(() => canvasStore.canvas?.current_node, updateNode)
} else {
let interval = setInterval(updateNode, 300)
onBeforeUnmount(() => clearInterval(this.interval))
}
updateNode()
</script>
<style scoped>
.doc-node {
font-size: 1.5em;
}
.doc-section {
background-color: var(--comfy-menu-bg);
}
.doc-item div {
margin-inline-start: 1vw;
}
@keyframes selectAnimation {
0% {
background-color: #5555;
}
80% {
background-color: #5555;
}
100% {
background-color: #0000;
}
}
.doc-item:focus {
animation: selectAnimation 2s;
}
</style>

View File

@@ -0,0 +1,18 @@
import { useQueuePendingTaskCountStore } from '@/stores/queueStore'
import { markRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import DocumentationSidebarTab from '@/components/sidebar/tabs/DocumentationSidebarTab.vue'
import type { SidebarTabExtension } from '@/types/extensionTypes'
export const useDocumentationSidebarTab = (): SidebarTabExtension => {
const { t } = useI18n()
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
return {
id: 'documentation',
icon: 'mdi mdi-help',
title: t('sideToolbar.documentation'),
tooltip: t('sideToolbar.documentation'),
component: markRaw(DocumentationSidebarTab),
type: 'vue'
}
}

View File

@@ -158,6 +158,7 @@ export default {
},
modelLibrary: 'Model Library',
downloads: 'Downloads',
documentation: 'Display documentation for nodes',
queueTab: {
showFlatList: 'Show Flat List',
backToAllTasks: 'Back to All Tasks',

View File

@@ -137,6 +137,7 @@ export class ComfyApp {
canvas: LGraphCanvas
dragOverNode: LGraphNode | null
canvasEl: HTMLCanvasElement
tooltipCallback?: (node: LGraphNode, name: string, value?: string) => boolean
// x, y, scale
zoom_drag_start: [number, number, number] | null
lastNodeErrors: any[] | null

View File

@@ -1,4 +1,5 @@
import { LGraphNode, LGraphGroup, LGraphCanvas } from '@comfyorg/litegraph'
import type { ComfyNodeItem } from '@/types/comfy'
import { defineStore } from 'pinia'
import { shallowRef } from 'vue'
@@ -10,6 +11,14 @@ export const useTitleEditorStore = defineStore('titleEditor', () => {
}
})
export const useHoveredItemStore = defineStore('hoveredItem', () => {
const hoveredItem = shallowRef<ComfyNodeItem | null>(null)
return {
hoveredItem
}
})
export const useCanvasStore = defineStore('canvas', () => {
/**
* The LGraphCanvas instance.

View File

@@ -5,7 +5,8 @@ import {
import {
type ComfyNodeDef,
type ComfyInputsSpec as ComfyInputsSpecSchema,
type InputSpec
type InputSpec,
type DescriptionSpec
} from '@/types/apiTypes'
import { defineStore } from 'pinia'
import { ComfyWidgetConstructor } from '@/scripts/widgets'
@@ -158,7 +159,7 @@ export class ComfyNodeDefImpl {
display_name: string
category: string
python_module: string
description: string
description: DescriptionSpec
deprecated: boolean
experimental: boolean
input: ComfyInputsSpec

View File

@@ -2,6 +2,7 @@ import { useModelLibrarySidebarTab } from '@/hooks/sidebarTabs/modelLibrarySideb
import { useNodeLibrarySidebarTab } from '@/hooks/sidebarTabs/nodeLibrarySidebarTab'
import { useQueueSidebarTab } from '@/hooks/sidebarTabs/queueSidebarTab'
import { useWorkflowsSidebarTab } from '@/hooks/sidebarTabs/workflowsSidebarTab'
import { useDocumentationSidebarTab } from '@/hooks/sidebarTabs/documentationSidebarTab'
import { SidebarTabExtension } from '@/types/extensionTypes'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
@@ -57,6 +58,7 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
registerSidebarTab(useNodeLibrarySidebarTab())
registerSidebarTab(useModelLibrarySidebarTab())
registerSidebarTab(useWorkflowsSidebarTab())
registerSidebarTab(useDocumentationSidebarTab())
}
return {

View File

@@ -356,6 +356,11 @@ const zComfyComboOutput = z.array(z.any())
const zComfyOutputTypesSpec = z.array(
z.union([zComfyNodeDataType, zComfyComboOutput])
)
const zDescriptionSpec = z.union([
z.string(),
z.tuple([z.string(), z.string()]),
z.tuple([z.string(), z.string(), z.record(z.string(), z.any())])
])
const zComfyNodeDef = z.object({
input: zComfyInputsSpec.optional(),
@@ -365,7 +370,7 @@ const zComfyNodeDef = z.object({
output_tooltips: z.array(z.string()).optional(),
name: z.string(),
display_name: z.string(),
description: z.string(),
description: zDescriptionSpec,
category: z.string(),
output_node: z.boolean(),
python_module: z.string(),
@@ -378,6 +383,7 @@ export type InputSpec = z.infer<typeof zInputSpec>
export type ComfyInputsSpec = z.infer<typeof zComfyInputsSpec>
export type ComfyOutputTypesSpec = z.infer<typeof zComfyOutputTypesSpec>
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>
export type DescriptionSpec = z.infer<typeof zDescriptionSpec>
export function validateComfyNodeDef(
data: any,

View File

@@ -1,6 +1,6 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import type { LGraphNode, IWidget } from '@comfyorg/litegraph'
import type { ComfyApp } from '@/scripts/app'
import type { ComfyNodeDef } from '@/types/apiTypes'
import type { ComfyNodeDef, DescriptionSpec } from '@/types/apiTypes'
import type { Keybinding } from '@/types/keyBindingTypes'
import type { ComfyCommand } from '@/stores/commandStore'
import type { SettingParams } from '@/types/settingTypes'
@@ -159,3 +159,9 @@ export interface ComfyExtension {
[key: string]: any
}
export type ComfyNodeItem =
| { node: LGraphNode; type: 'Title' }
| { node: LGraphNode; type: 'Output'; outputSlot: number }
| { node: LGraphNode; type: 'Input'; inputName: string }
| { node: LGraphNode; type: 'Widget'; widget: IWidget }