diff --git a/browser_tests/documentationSidebar.spec.ts b/browser_tests/documentationSidebar.spec.ts
new file mode 100644
index 000000000..7937329a7
--- /dev/null
+++ b/browser_tests/documentationSidebar.spec.ts
@@ -0,0 +1,130 @@
+import { expect } from '@playwright/test'
+import { comfyPageFixture as test } from './ComfyPage'
+const nodeDef = {
+ title: 'TestNodeAdvancedDoc'
+}
+
+test.describe('Documentation Sidebar', () => {
+ test.beforeEach(async ({ comfyPage }) => {
+ await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
+ await comfyPage.loadWorkflow('default')
+ })
+
+ test.afterEach(async ({ comfyPage }) => {
+ const currentThemeId = await comfyPage.menu.getThemeId()
+ if (currentThemeId !== 'dark') {
+ await comfyPage.menu.toggleTheme()
+ }
+ await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
+ })
+
+ 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)
+ await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
+ })
+ test.afterEach(async ({ comfyPage }) => {
+ await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
+ })
+ 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(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.
+
Can include arbitrary html
+or out of order widgets
+`,
+ {}
+ ]
+}
diff --git a/src/components/graph/NodeTooltip.vue b/src/components/graph/NodeTooltip.vue
index 643c60db3..1e71d9ac7 100644
--- a/src/components/graph/NodeTooltip.vue
+++ b/src/components/graph/NodeTooltip.vue
@@ -10,20 +10,23 @@
diff --git a/src/hooks/sidebarTabs/documentationSidebarTab.ts b/src/hooks/sidebarTabs/documentationSidebarTab.ts
new file mode 100644
index 000000000..87356c128
--- /dev/null
+++ b/src/hooks/sidebarTabs/documentationSidebarTab.ts
@@ -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'
+ }
+}
diff --git a/src/locales/en.ts b/src/locales/en.ts
index c5cb9f928..cb8558233 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -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',
diff --git a/src/scripts/app.ts b/src/scripts/app.ts
index d5f24f4bd..248250413 100644
--- a/src/scripts/app.ts
+++ b/src/scripts/app.ts
@@ -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
diff --git a/src/stores/graphStore.ts b/src/stores/graphStore.ts
index 268f82255..6a70f93a8 100644
--- a/src/stores/graphStore.ts
+++ b/src/stores/graphStore.ts
@@ -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(null)
+
+ return {
+ hoveredItem
+ }
+})
+
export const useCanvasStore = defineStore('canvas', () => {
/**
* The LGraphCanvas instance.
diff --git a/src/stores/nodeDefStore.ts b/src/stores/nodeDefStore.ts
index dac62d4fb..8298fe98c 100644
--- a/src/stores/nodeDefStore.ts
+++ b/src/stores/nodeDefStore.ts
@@ -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
diff --git a/src/stores/workspace/sidebarTabStore.ts b/src/stores/workspace/sidebarTabStore.ts
index 9154bd99f..5e564dd55 100644
--- a/src/stores/workspace/sidebarTabStore.ts
+++ b/src/stores/workspace/sidebarTabStore.ts
@@ -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 {
diff --git a/src/types/apiTypes.ts b/src/types/apiTypes.ts
index 3081e994e..79a07f616 100644
--- a/src/types/apiTypes.ts
+++ b/src/types/apiTypes.ts
@@ -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
export type ComfyInputsSpec = z.infer
export type ComfyOutputTypesSpec = z.infer
export type ComfyNodeDef = z.infer
+export type DescriptionSpec = z.infer
export function validateComfyNodeDef(
data: any,
diff --git a/src/types/comfy.ts b/src/types/comfy.ts
index 024fbbe32..b3f070dce 100644
--- a/src/types/comfy.ts
+++ b/src/types/comfy.ts
@@ -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 }