Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a sidebar tab for documentation #1103

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7eaa54f
Initial sidebar documentation implementation
AustinMroz Sep 24, 2024
da936d6
Remove unused styling code, unwrap
AustinMroz Sep 24, 2024
8160ca0
Formatting improvements,
AustinMroz Sep 26, 2024
4aa04d1
type implementation for detailed descriptions
AustinMroz Sep 27, 2024
a8ac729
Theming, pruning, and optional callbacks
AustinMroz Sep 30, 2024
1d0ae76
Connect hover functionality, scroll fixes
AustinMroz Sep 30, 2024
7a5d39f
Temporarily highlight item doc item on selection
AustinMroz Oct 1, 2024
44f900e
Typing fixes, initial tests
AustinMroz Oct 3, 2024
52933e1
Properly handle theme with css variables
AustinMroz Oct 3, 2024
3b679f1
Return styling to body, streamline tests
AustinMroz Oct 3, 2024
bc66307
Move render callback to trigger on node change
AustinMroz Oct 3, 2024
95cec85
Move css to style.css
AustinMroz Oct 4, 2024
95a4fe7
Port sidebar documentation to vue component
AustinMroz Oct 7, 2024
b2ef66e
Update tooltip handling
AustinMroz Oct 8, 2024
f8ba0ab
Migrate to new sidebar registration
AustinMroz Oct 8, 2024
214f48a
Functional node swaps under vue, icon update
AustinMroz Oct 9, 2024
9263330
Update tests for vue port
AustinMroz Oct 10, 2024
f48594f
Properly mirror the new description type
AustinMroz Oct 15, 2024
1dfcc7a
Migrate tooltip tracking to a pinia store
AustinMroz Oct 18, 2024
3252d62
Fix missing await
AustinMroz Oct 19, 2024
293f429
Add tests for advanced description formats
AustinMroz Nov 13, 2024
ef19103
Merge sidebar-documentation onto main
AustinMroz Nov 27, 2024
ce9cfdb
Check for swapped node on interval.
AustinMroz Nov 28, 2024
89027ea
Update tests
AustinMroz Nov 28, 2024
867c50f
Prune dead code, tighten interval, add screenshot
AustinMroz Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions browser_tests/documentationSidebar.spec.ts
Original file line number Diff line number Diff line change
@@ -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>
`,
{}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 43 additions & 10 deletions src/components/graph/NodeTooltip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,20 +46,52 @@ 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
const node = canvas.node_over
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
Expand All @@ -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(
Expand All @@ -79,28 +114,26 @@ 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
idleTimeout = window.setTimeout(onIdle, 500)
}

useEventListener(window, 'mousemove', onMouseMove)
useEventListener(window, 'click', hideTooltip)
useEventListener(window, 'click', clearHovered)
</script>

<style lang="css" scoped>
Expand Down
6 changes: 5 additions & 1 deletion src/components/node/NodePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading
Loading