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 }