From 7eaa54fe3f56702f796ce444f5745ca02f98af5f Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 24 Sep 2024 01:23:08 -0500 Subject: [PATCH 01/24] Initial sidebar documentation implementation --- src/extensions/core/documentationSidebar.ts | 243 ++++++++++++++++++++ src/extensions/core/index.ts | 1 + 2 files changed, 244 insertions(+) create mode 100644 src/extensions/core/documentationSidebar.ts diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts new file mode 100644 index 000000000..66675213c --- /dev/null +++ b/src/extensions/core/documentationSidebar.ts @@ -0,0 +1,243 @@ +import { app } from '../../scripts/app.js' +import { api } from '../../scripts/api.js' + +let iconOverride = document.createElement('style') +iconOverride.innerHTML = `.VHSTestIcon:before {font-size: 1.5em; content: '?';}` +document.body.append(iconOverride) + +var helpDOM +helpDOM = document.createElement('div') + +function initHelpDOM() { + helpDOM.className = 'litegraph' + let scrollbarStyle = document.createElement('style') + scrollbarStyle.innerHTML = ` + * { + scrollbar-width: 6px; + scrollbar-color: #0003 #0000; + } + ::-webkit-scrollbar { + background: transparent; + width: 6px; + } + ::-webkit-scrollbar-thumb { + background: #0005; + border-radius: 20px + } + ::-webkit-scrollbar-button { + display: none; + } + .VHS_loopedvideo::-webkit-media-controls-mute-button { + display:none; + } + .VHS_loopedvideo::-webkit-media-controls-fullscreen-button { + display:none; + } + ` + scrollbarStyle.id = 'scroll-properties' + parentDOM.appendChild(scrollbarStyle) + function setCollapse(el, doCollapse) { + if (doCollapse) { + el.children[0].children[0].innerHTML = '+' + Object.assign(el.children[1].style, { + color: '#CCC', + overflowX: 'hidden', + width: '0px', + minWidth: 'calc(100% - 20px)', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + }) + for (let child of el.children[1].children) { + if (child.style.display != 'none') { + child.origDisplay = child.style.display + } + child.style.display = 'none' + } + } else { + el.children[0].children[0].innerHTML = '-' + Object.assign(el.children[1].style, { + color: '', + overflowX: '', + width: '100%', + minWidth: '', + textOverflow: '', + whiteSpace: '' + }) + for (let child of el.children[1].children) { + child.style.display = child.origDisplay + } + } + } + helpDOM.collapseOnClick = function () { + let doCollapse = this.children[0].innerHTML == '-' + setCollapse(this.parentElement, doCollapse) + } + helpDOM.selectHelp = function (name, value) { + //attempt to navigate to name in help + function collapseUnlessMatch(items, t) { + var match = items.querySelector('[vhs_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 + //This has the unfortunate side effect of trying to scroll the main + //window if the documentation windows is forcibly offscreen, + //but it's easy to simply scroll the main window back and seems to + //have no visual side effects + match.scrollIntoView(false) + window.scrollTo(0, 0) + for (let i of items.querySelectorAll('.VHS_collapse')) { + if (i.contains(match)) { + setCollapse(i, false) + } else { + setCollapse(i, true) + } + } + return match + } + let target = collapseUnlessMatch(helpDOM, name) + if (target && value) { + collapseUnlessMatch(target, value) + } + } + helpDOM.addHelp = function (node, nodeType, description) { + let timeout = null + chainCallback(node, 'onMouseMove', function (e, pos, canvas) { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + if (helpDOM.node != this) { + return + } + timeout = setTimeout(() => { + let n = this + if ( + pos[0] > 0 && + pos[0] < n.size[0] && + pos[1] > 0 && + pos[1] < n.size[1] + ) { + //TODO: provide help specific to element clicked + let inputRows = Math.max( + n.inputs?.length || 0, + n.outputs?.length || 0 + ) + if (pos[1] < LiteGraph.NODE_SLOT_HEIGHT * inputRows) { + let row = Math.floor((pos[1] - 7) / LiteGraph.NODE_SLOT_HEIGHT) + if (pos[0] < n.size[0] / 2) { + if (row < n.inputs.length) { + helpDOM.selectHelp(n.inputs[row].name) + } + } else { + if (row < n.outputs.length) { + helpDOM.selectHelp(n.outputs[row].name) + } + } + } else { + //probably widget, but widgets have variable height. + let basey = LiteGraph.NODE_SLOT_HEIGHT * inputRows + 6 + for (let w of n.widgets) { + if (w.y) { + basey = w.y + } + let wheight = LiteGraph.NODE_WIDGET_HEIGHT + 4 + if (w.computeSize) { + wheight = w.computeSize(n.size[0])[1] + } + if (pos[1] < basey + wheight) { + helpDOM.selectHelp(w.name, w.value) + break + } + basey += wheight + } + } + } + }, 500) + }) + chainCallback(node, 'onMouseLeave', function (e, pos, canvas) { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + }) + } +} +function updateNode(node) { + //Always use latest node. If it lacks documentation, that should be communicated + //instead of confusing users by picking a different recent node that does + node ||= app.graph._nodes[app.graph._nodes.length - 1] + const def = LiteGraph.getNodeType(node.type).nodeData + if (helpDOM.def == def) { + return + } + helpDOM.def = def + if (def.longDescription) { + helpDOM.innerHTML = def.longDescription + } else { + //do additional parsing to prettify output and combine tooltips + let content = '' + if (def.description) { + content += def.description + } + let inputs = [] + for (let input in def?.input?.required || {}) { + if (def.input.required[input][1]?.tooltip) { + inputs.push( + '' + input + ': ' + def.input.required[input][1].tooltip + ) + } + } + for (let input in def?.input?.optional || {}) { + if (def.input.optional[input][1]?.tooltip) { + inputs.push( + '' + input + ': ' + def.input.optional[input][1].tooltip + ) + } + } + if (inputs.length) { + content += '

' + inputs.join('
') + '
' + } + if (def.output_tooltips) { + content += '

' + let outputs = def.output_name || def.output + for (let i = 0; i < outputs.length; i++) { + content += + '
' + outputs[i] + ': ' + def.output_tooltips[i] + '
' + } + } + helpDOM.innerHTML = content + } +} +var bringToFront + +let documentationSidebar = { + id: 'documentationSidebar', + title: 'Documentation', + icon: 'VHSTestIcon', + type: 'custom', + render: (e) => { + if (!bringToFront) { + var bringToFront = app.canvas.bringToFront + app.canvas.bringToFront = function (node) { + updateNode(node) + return bringToFront.apply(this, arguments) + } + } + updateNode() + e.parentElement.style.overflowX = '' + if (!e?.children?.length) { + e.appendChild(helpDOM) + } + } +} +app.extensionManager.registerSidebarTab(documentationSidebar) diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index 772dd5f3d..304a11d60 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -22,3 +22,4 @@ import './webcamCapture' import './widgetInputs' import './uploadAudio' import './nodeBadge' +import './documentationSidebar' From da936d69b61020844bfa0adbcac93fbf22f67f8c Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 24 Sep 2024 18:56:22 -0500 Subject: [PATCH 02/24] Remove unused styling code, unwrap --- src/extensions/core/documentationSidebar.ts | 274 +++++++++----------- 1 file changed, 125 insertions(+), 149 deletions(-) diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index 66675213c..e93e65254 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -8,169 +8,137 @@ document.body.append(iconOverride) var helpDOM helpDOM = document.createElement('div') -function initHelpDOM() { - helpDOM.className = 'litegraph' - let scrollbarStyle = document.createElement('style') - scrollbarStyle.innerHTML = ` - * { - scrollbar-width: 6px; - scrollbar-color: #0003 #0000; - } - ::-webkit-scrollbar { - background: transparent; - width: 6px; - } - ::-webkit-scrollbar-thumb { - background: #0005; - border-radius: 20px - } - ::-webkit-scrollbar-button { - display: none; - } - .VHS_loopedvideo::-webkit-media-controls-mute-button { - display:none; - } - .VHS_loopedvideo::-webkit-media-controls-fullscreen-button { - display:none; - } - ` - scrollbarStyle.id = 'scroll-properties' - parentDOM.appendChild(scrollbarStyle) - function setCollapse(el, doCollapse) { - if (doCollapse) { - el.children[0].children[0].innerHTML = '+' - Object.assign(el.children[1].style, { - color: '#CCC', - overflowX: 'hidden', - width: '0px', - minWidth: 'calc(100% - 20px)', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap' - }) - for (let child of el.children[1].children) { - if (child.style.display != 'none') { - child.origDisplay = child.style.display - } - child.style.display = 'none' - } - } else { - el.children[0].children[0].innerHTML = '-' - Object.assign(el.children[1].style, { - color: '', - overflowX: '', - width: '100%', - minWidth: '', - textOverflow: '', - whiteSpace: '' - }) - for (let child of el.children[1].children) { - child.style.display = child.origDisplay +function setCollapse(el, doCollapse) { + if (doCollapse) { + el.children[0].children[0].innerHTML = '+' + Object.assign(el.children[1].style, { + color: '#CCC', + overflowX: 'hidden', + width: '0px', + minWidth: 'calc(100% - 20px)', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + }) + for (let child of el.children[1].children) { + if (child.style.display != 'none') { + child.origDisplay = child.style.display } + child.style.display = 'none' + } + } else { + el.children[0].children[0].innerHTML = '-' + Object.assign(el.children[1].style, { + color: '', + overflowX: '', + width: '100%', + minWidth: '', + textOverflow: '', + whiteSpace: '' + }) + for (let child of el.children[1].children) { + child.style.display = child.origDisplay } } - helpDOM.collapseOnClick = function () { - let doCollapse = this.children[0].innerHTML == '-' - setCollapse(this.parentElement, doCollapse) - } - helpDOM.selectHelp = function (name, value) { - //attempt to navigate to name in help - function collapseUnlessMatch(items, t) { - var match = items.querySelector('[vhs_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 - //This has the unfortunate side effect of trying to scroll the main - //window if the documentation windows is forcibly offscreen, - //but it's easy to simply scroll the main window back and seems to - //have no visual side effects - match.scrollIntoView(false) - window.scrollTo(0, 0) - for (let i of items.querySelectorAll('.VHS_collapse')) { - if (i.contains(match)) { - setCollapse(i, false) - } else { - setCollapse(i, true) +} +helpDOM.collapseOnClick = function () { + let doCollapse = this.children[0].innerHTML == '-' + setCollapse(this.parentElement, doCollapse) +} +helpDOM.selectHelp = function (name, value) { + //attempt to navigate to name in help + function collapseUnlessMatch(items, t) { + var match = items.querySelector('[vhs_title="' + t + '"]') + if (!match) { + for (let i of items.children) { + if (i.innerHTML.slice(0, t.length + 5).includes(t)) { + match = i + break } } - return match } - let target = collapseUnlessMatch(helpDOM, name) - if (target && value) { - collapseUnlessMatch(target, value) + if (!match) { + return null } - } - helpDOM.addHelp = function (node, nodeType, description) { - let timeout = null - chainCallback(node, 'onMouseMove', function (e, pos, canvas) { - if (timeout) { - clearTimeout(timeout) - timeout = null - } - if (helpDOM.node != this) { - return + //For longer documentation items with fewer collapsable elements, + //scroll to make sure the entirety of the selected item is visible + //This has the unfortunate side effect of trying to scroll the main + //window if the documentation windows is forcibly offscreen, + //but it's easy to simply scroll the main window back and seems to + //have no visual side effects + match.scrollIntoView(false) + window.scrollTo(0, 0) + for (let i of items.querySelectorAll('.VHS_collapse')) { + if (i.contains(match)) { + setCollapse(i, false) + } else { + setCollapse(i, true) } - timeout = setTimeout(() => { - let n = this - if ( - pos[0] > 0 && - pos[0] < n.size[0] && - pos[1] > 0 && - pos[1] < n.size[1] - ) { - //TODO: provide help specific to element clicked - let inputRows = Math.max( - n.inputs?.length || 0, - n.outputs?.length || 0 - ) - if (pos[1] < LiteGraph.NODE_SLOT_HEIGHT * inputRows) { - let row = Math.floor((pos[1] - 7) / LiteGraph.NODE_SLOT_HEIGHT) - if (pos[0] < n.size[0] / 2) { - if (row < n.inputs.length) { - helpDOM.selectHelp(n.inputs[row].name) - } - } else { - if (row < n.outputs.length) { - helpDOM.selectHelp(n.outputs[row].name) - } + } + return match + } + let target = collapseUnlessMatch(helpDOM, name) + if (target && value) { + collapseUnlessMatch(target, value) + } +} +helpDOM.addHelp = function (node, nodeType, description) { + let timeout = null + chainCallback(node, 'onMouseMove', function (e, pos, canvas) { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + if (helpDOM.node != this) { + return + } + timeout = setTimeout(() => { + let n = this + if ( + pos[0] > 0 && + pos[0] < n.size[0] && + pos[1] > 0 && + pos[1] < n.size[1] + ) { + //TODO: provide help specific to element clicked + let inputRows = Math.max(n.inputs?.length || 0, n.outputs?.length || 0) + if (pos[1] < LiteGraph.NODE_SLOT_HEIGHT * inputRows) { + let row = Math.floor((pos[1] - 7) / LiteGraph.NODE_SLOT_HEIGHT) + if (pos[0] < n.size[0] / 2) { + if (row < n.inputs.length) { + helpDOM.selectHelp(n.inputs[row].name) } } else { - //probably widget, but widgets have variable height. - let basey = LiteGraph.NODE_SLOT_HEIGHT * inputRows + 6 - for (let w of n.widgets) { - if (w.y) { - basey = w.y - } - let wheight = LiteGraph.NODE_WIDGET_HEIGHT + 4 - if (w.computeSize) { - wheight = w.computeSize(n.size[0])[1] - } - if (pos[1] < basey + wheight) { - helpDOM.selectHelp(w.name, w.value) - break - } - basey += wheight + if (row < n.outputs.length) { + helpDOM.selectHelp(n.outputs[row].name) } } + } else { + //probably widget, but widgets have variable height. + let basey = LiteGraph.NODE_SLOT_HEIGHT * inputRows + 6 + for (let w of n.widgets) { + if (w.y) { + basey = w.y + } + let wheight = LiteGraph.NODE_WIDGET_HEIGHT + 4 + if (w.computeSize) { + wheight = w.computeSize(n.size[0])[1] + } + if (pos[1] < basey + wheight) { + helpDOM.selectHelp(w.name, w.value) + break + } + basey += wheight + } } - }, 500) - }) - chainCallback(node, 'onMouseLeave', function (e, pos, canvas) { - if (timeout) { - clearTimeout(timeout) - timeout = null } - }) - } + }, 500) + }) + chainCallback(node, 'onMouseLeave', function (e, pos, canvas) { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + }) } function updateNode(node) { //Always use latest node. If it lacks documentation, that should be communicated @@ -219,6 +187,14 @@ function updateNode(node) { } } var bringToFront +app.registerExtension({ + name: 'Comfy.longformDocumentation', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (typeof nodeData.description == 'Object') { + nodeData.description = 'redirected' + } + } +}) let documentationSidebar = { id: 'documentationSidebar', From 8160ca0342200f1133aa58a0aaeb6a939ff9f802 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Wed, 25 Sep 2024 20:56:03 -0500 Subject: [PATCH 03/24] 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 --- src/extensions/core/documentationSidebar.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index e93e65254..9cb2d46f4 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -183,19 +183,22 @@ function updateNode(node) { '
' + outputs[i] + ': ' + def.output_tooltips[i] + '
' } } + if (content == '') { + content = 'No documentation available' + } helpDOM.innerHTML = content } } -var bringToFront app.registerExtension({ name: 'Comfy.longformDocumentation', async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (typeof nodeData.description == 'Object') { - nodeData.description = 'redirected' + //TODO: Find better method. Likely require explicit opt in + if (nodeData.description.includes('') && !nodeData.longDescription) { + nodeData.longDescription = nodeData.description } } }) - +var bringToFront let documentationSidebar = { id: 'documentationSidebar', title: 'Documentation', From 4aa04d1419ffe23ee5bc8efd908fda2fc5340839 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Fri, 27 Sep 2024 16:55:28 -0500 Subject: [PATCH 04/24] 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 --- src/components/graph/NodeTooltip.vue | 3 +++ src/components/node/NodePreview.vue | 6 +++++- src/extensions/core/documentationSidebar.ts | 13 ++----------- src/types/apiTypes.ts | 7 ++++++- src/types/comfy.d.ts | 2 +- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/graph/NodeTooltip.vue b/src/components/graph/NodeTooltip.vue index 8054f86a8..f473cb3a5 100644 --- a/src/components/graph/NodeTooltip.vue +++ b/src/components/graph/NodeTooltip.vue @@ -85,6 +85,9 @@ const onIdle = () => { 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 ) { + if (Array.isArray(nodeDef.description)) { + return showTooltip(nodeDef.description[0]) + } return showTooltip(nodeDef.description) } diff --git a/src/components/node/NodePreview.vue b/src/components/node/NodePreview.vue index 4046db39d..064962e3e 100644 --- a/src/components/node/NodePreview.vue +++ b/src/components/node/NodePreview.vue @@ -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 + }} diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index 9cb2d46f4..ba49d8f1f 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -149,8 +149,8 @@ function updateNode(node) { return } helpDOM.def = def - if (def.longDescription) { - helpDOM.innerHTML = def.longDescription + if (Array.isArray(def.description)) { + helpDOM.innerHTML = def.description[1] } else { //do additional parsing to prettify output and combine tooltips let content = '' @@ -189,15 +189,6 @@ function updateNode(node) { helpDOM.innerHTML = content } } -app.registerExtension({ - name: 'Comfy.longformDocumentation', - async beforeRegisterNodeDef(nodeType, nodeData, app) { - //TODO: Find better method. Likely require explicit opt in - if (nodeData.description.includes('') && !nodeData.longDescription) { - nodeData.longDescription = nodeData.description - } - } -}) var bringToFront let documentationSidebar = { id: 'documentationSidebar', diff --git a/src/types/apiTypes.ts b/src/types/apiTypes.ts index fced6cc35..f3c5710d9 100644 --- a/src/types/apiTypes.ts +++ b/src/types/apiTypes.ts @@ -345,6 +345,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(), @@ -354,7 +359,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(), diff --git a/src/types/comfy.d.ts b/src/types/comfy.d.ts index 7704e3d60..db9b8eb60 100644 --- a/src/types/comfy.d.ts +++ b/src/types/comfy.d.ts @@ -117,7 +117,7 @@ export interface ComfyExtension { export type ComfyObjectInfo = { name: string display_name?: string - description?: string + description?: [string | string, string | string, string, Record] category: string input?: { required?: Record From a8ac7296c24b9fe3244c9a4bb0553a4e7f2ae205 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 30 Sep 2024 14:40:54 -0500 Subject: [PATCH 05/24] 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. --- src/extensions/core/documentationSidebar.ts | 150 ++++++++------------ 1 file changed, 63 insertions(+), 87 deletions(-) diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index ba49d8f1f..3635c4675 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -1,12 +1,8 @@ -import { app } from '../../scripts/app.js' -import { api } from '../../scripts/api.js' +import { app } from '../../scripts/app' +import { api } from '../../scripts/api' +import { getColorPalette } from './colorPalette' -let iconOverride = document.createElement('style') -iconOverride.innerHTML = `.VHSTestIcon:before {font-size: 1.5em; content: '?';}` -document.body.append(iconOverride) - -var helpDOM -helpDOM = document.createElement('div') +var helpDOM = document.createElement('div') function setCollapse(el, doCollapse) { if (doCollapse) { @@ -44,10 +40,16 @@ helpDOM.collapseOnClick = function () { let doCollapse = this.children[0].innerHTML == '-' setCollapse(this.parentElement, doCollapse) } -helpDOM.selectHelp = function (name, value) { +//TODO: connect with doc tooltips +//If doc sidebar is opened, the current node should not display tooltips, +//but navigate the sidebar pane as appropriate. +helpDOM.selectHelp = function (name: string, value?: string) { + if (helpDOM.def[2].select) { + return helpDOM.def[2].select(this, name, value) + } //attempt to navigate to name in help function collapseUnlessMatch(items, t) { - var match = items.querySelector('[vhs_title="' + 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)) { @@ -61,13 +63,12 @@ helpDOM.selectHelp = function (name, value) { } //For longer documentation items with fewer collapsable elements, //scroll to make sure the entirety of the selected item is visible - //This has the unfortunate side effect of trying to scroll the main - //window if the documentation windows is forcibly offscreen, - //but it's easy to simply scroll the main window back and seems to - //have no visual side effects match.scrollIntoView(false) - window.scrollTo(0, 0) - for (let i of items.querySelectorAll('.VHS_collapse')) { + //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 { @@ -81,65 +82,6 @@ helpDOM.selectHelp = function (name, value) { collapseUnlessMatch(target, value) } } -helpDOM.addHelp = function (node, nodeType, description) { - let timeout = null - chainCallback(node, 'onMouseMove', function (e, pos, canvas) { - if (timeout) { - clearTimeout(timeout) - timeout = null - } - if (helpDOM.node != this) { - return - } - timeout = setTimeout(() => { - let n = this - if ( - pos[0] > 0 && - pos[0] < n.size[0] && - pos[1] > 0 && - pos[1] < n.size[1] - ) { - //TODO: provide help specific to element clicked - let inputRows = Math.max(n.inputs?.length || 0, n.outputs?.length || 0) - if (pos[1] < LiteGraph.NODE_SLOT_HEIGHT * inputRows) { - let row = Math.floor((pos[1] - 7) / LiteGraph.NODE_SLOT_HEIGHT) - if (pos[0] < n.size[0] / 2) { - if (row < n.inputs.length) { - helpDOM.selectHelp(n.inputs[row].name) - } - } else { - if (row < n.outputs.length) { - helpDOM.selectHelp(n.outputs[row].name) - } - } - } else { - //probably widget, but widgets have variable height. - let basey = LiteGraph.NODE_SLOT_HEIGHT * inputRows + 6 - for (let w of n.widgets) { - if (w.y) { - basey = w.y - } - let wheight = LiteGraph.NODE_WIDGET_HEIGHT + 4 - if (w.computeSize) { - wheight = w.computeSize(n.size[0])[1] - } - if (pos[1] < basey + wheight) { - helpDOM.selectHelp(w.name, w.value) - break - } - basey += wheight - } - } - } - }, 500) - }) - chainCallback(node, 'onMouseLeave', function (e, pos, canvas) { - if (timeout) { - clearTimeout(timeout) - timeout = null - } - }) -} function updateNode(node) { //Always use latest node. If it lacks documentation, that should be communicated //instead of confusing users by picking a different recent node that does @@ -155,45 +97,61 @@ function updateNode(node) { //do additional parsing to prettify output and combine tooltips let content = '' if (def.description) { - content += def.description + content += '
' + def.description + '
' } let inputs = [] for (let input in def?.input?.required || {}) { if (def.input.required[input][1]?.tooltip) { - inputs.push( - '' + input + ': ' + def.input.required[input][1].tooltip - ) + inputs.push([input, def.input.required[input][1].tooltip]) } } for (let input in def?.input?.optional || {}) { if (def.input.optional[input][1]?.tooltip) { - inputs.push( - '' + input + ': ' + def.input.optional[input][1].tooltip - ) + inputs.push([input, def.input.optional[input][1].tooltip]) } } if (inputs.length) { - content += '

' + inputs.join('
') + '
' + content += '
Inputs
' + for (let [k, v] of inputs) { + content += '
' + k + '
' + v + '
' + } + //content += "" + //content += '

' + inputs.join('
') + '
' } if (def.output_tooltips) { - content += '

' + content += '
Outputs
' let outputs = def.output_name || def.output for (let i = 0; i < outputs.length; i++) { content += - '
' + outputs[i] + ': ' + def.output_tooltips[i] + '
' + '
' + + outputs[i] + + '
' + + def.output_tooltips[i] + + '
' } + //outputs += '' } if (content == '') { content = 'No documentation available' } + content = '
' + def.display_name + '
' + content helpDOM.innerHTML = content } } +let docStyleElement = document.createElement('style') +let documentationStyle = ` +.DocumentationIcon:before { + font-size: 1.5em; content: '?'; +} +` +docStyleElement.innerHTML = documentationStyle +document.body.append(docStyleElement) + var bringToFront let documentationSidebar = { id: 'documentationSidebar', title: 'Documentation', - icon: 'VHSTestIcon', + icon: 'DocumentationIcon', type: 'custom', render: (e) => { if (!bringToFront) { @@ -203,11 +161,29 @@ let documentationSidebar = { return bringToFront.apply(this, arguments) } } + //TODO: properly update colors when theme is toggled + let documentationStyle = ` + .doc-node { + font-size: 1.5em + } + .doc-section { + background-color: ${getColorPalette().colors.comfy_base['tr-odd-bg-color']} + } + .doc-item { + margin-inline-start: 1vw; + } + .DocumentationIcon:before { + font-size: 1.5em; content: '?'; + } + ` + docStyleElement.innerHTML = documentationStyle updateNode() - e.parentElement.style.overflowX = '' if (!e?.children?.length) { e.appendChild(helpDOM) } + if (helpDOM.def.description[2]?.render) { + helpDOM.def.description[2].render(e) + } } } app.extensionManager.registerSidebarTab(documentationSidebar) From 1d0ae76f8ceb6d5043efe7c5f09c45944546d819 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 30 Sep 2024 18:00:34 -0500 Subject: [PATCH 06/24] 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 --- src/components/graph/NodeTooltip.vue | 21 ++++++++---- src/extensions/core/documentationSidebar.ts | 37 ++++++++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/components/graph/NodeTooltip.vue b/src/components/graph/NodeTooltip.vue index f473cb3a5..fd3015bdd 100644 --- a/src/components/graph/NodeTooltip.vue +++ b/src/components/graph/NodeTooltip.vue @@ -85,7 +85,9 @@ const onIdle = () => { 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 ) { - if (Array.isArray(nodeDef.description)) { + if (comfyApp?.tooltipCallback?.(node, 'DESCRIPTION')) { + return + } else if (Array.isArray(nodeDef.description)) { return showTooltip(nodeDef.description[0]) } return showTooltip(nodeDef.description) @@ -101,7 +103,9 @@ const onIdle = () => { ) if (inputSlot !== -1) { const inputName = node.inputs[inputSlot].name - return showTooltip(nodeDef.input.getInput(inputName)?.tooltip) + if (!comfyApp?.tooltipCallback?.(node, inputName)) { + return showTooltip(nodeDef.input.getInput(inputName)?.tooltip) + } } const outputSlot = canvas.isOverNodeOutput( @@ -111,15 +115,20 @@ const onIdle = () => { [0, 0] ) if (outputSlot !== -1) { - return showTooltip(nodeDef.output.all?.[outputSlot]?.tooltip) + const outputDef = nodeDef.output.all?.[outputSlot] + if (!comfyApp?.tooltipCallback?.(node, outputDef?.name)) { + return showTooltip(outputDef?.tooltip) + } } const widget = getHoveredWidget() // 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 - ) + if (!comfyApp?.tooltipCallback?.(node, widget.name, widget.value)) { + return showTooltip( + widget.tooltip ?? nodeDef.input.getInput(widget.name)?.tooltip + ) + } } } diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index 3635c4675..31faa6a63 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -44,7 +44,7 @@ helpDOM.collapseOnClick = function () { //If doc sidebar is opened, the current node should not display tooltips, //but navigate the sidebar pane as appropriate. helpDOM.selectHelp = function (name: string, value?: string) { - if (helpDOM.def[2].select) { + if (helpDOM.def[2]?.select) { return helpDOM.def[2].select(this, name, value) } //attempt to navigate to name in help @@ -63,7 +63,7 @@ helpDOM.selectHelp = function (name: string, value?: string) { } //For longer documentation items with fewer collapsable elements, //scroll to make sure the entirety of the selected item is visible - match.scrollIntoView(false) + 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 @@ -78,10 +78,23 @@ helpDOM.selectHelp = function (name: string, value?: string) { return match } let target = collapseUnlessMatch(helpDOM, name) - if (target && value) { - collapseUnlessMatch(target, value) + if (target) { + target.focus() + if (value) { + collapseUnlessMatch(target, value) + } } } +app.tooltipCallback = function (node, name, value) { + if (node != app.graph._nodes[app.graph._nodes.length - 1]) { + return false + } + if (name == 'DESCRIPTION') { + return false + } + helpDOM.selectHelp(name, value) + return true +} function updateNode(node) { //Always use latest node. If it lacks documentation, that should be communicated //instead of confusing users by picking a different recent node that does @@ -113,7 +126,12 @@ function updateNode(node) { if (inputs.length) { content += '
Inputs
' for (let [k, v] of inputs) { - content += '
' + k + '
' + v + '
' + content += + '
' + + k + + '
' + + v + + '
' } //content += "" //content += '

' + inputs.join('
') + '
' @@ -123,9 +141,9 @@ function updateNode(node) { let outputs = def.output_name || def.output for (let i = 0; i < outputs.length; i++) { content += - '
' + + '
' + outputs[i] + - '
' + + '
' + def.output_tooltips[i] + '
' } @@ -169,9 +187,12 @@ let documentationSidebar = { .doc-section { background-color: ${getColorPalette().colors.comfy_base['tr-odd-bg-color']} } - .doc-item { + .doc-item div { margin-inline-start: 1vw; } + .doc-item:focus { + background-color: #666 + } .DocumentationIcon:before { font-size: 1.5em; content: '?'; } From 7a5d39f41f517ce7bd8f3a253f30aecedc8a281e Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 1 Oct 2024 00:20:13 -0500 Subject: [PATCH 07/24] Temporarily highlight item doc item on selection --- src/extensions/core/documentationSidebar.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index 31faa6a63..aa640d835 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -185,13 +185,18 @@ let documentationSidebar = { font-size: 1.5em } .doc-section { - background-color: ${getColorPalette().colors.comfy_base['tr-odd-bg-color']} + background-color: ${getColorPalette().colors.comfy_base['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 { - background-color: #666 + animation: selectAnimation 2s; } .DocumentationIcon:before { font-size: 1.5em; content: '?'; From 44f900ef56f405bb2e13553f06a8ef00cad206c4 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Thu, 3 Oct 2024 00:13:46 -0500 Subject: [PATCH 08/24] Typing fixes, initial tests --- browser_tests/documentationSidebar.spec.ts | 63 +++++++++++++++++++++ src/extensions/core/documentationSidebar.ts | 42 +++++++------- src/scripts/app.ts | 1 + 3 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 browser_tests/documentationSidebar.spec.ts diff --git a/browser_tests/documentationSidebar.spec.ts b/browser_tests/documentationSidebar.spec.ts new file mode 100644 index 000000000..b2c51865f --- /dev/null +++ b/browser_tests/documentationSidebar.spec.ts @@ -0,0 +1,63 @@ +import { expect } from '@playwright/test' +import { comfyPageFixture as test } from './ComfyPage' + +test.describe('Documentation Sidebar', () => { + test('Sidebar registered', async ({ comfyPage }) => { + await expect( + comfyPage.page.locator('.documentationSidebar-tab-button') + ).toBeVisible() + }) + test('Parses help for basic node', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('default') + await comfyPage.page.locator('.documentationSidebar-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.loadWorkflow('default') + await comfyPage.page.locator('.documentationSidebar-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( + '.side-bar-panel > div > div > div > div:nth-child(4)' + ) + ).toBeFocused() + }) + test('Updates when a new node is selected', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('default') + await comfyPage.page.locator('.documentationSidebar-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.describe('Theming', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.ColorPalette', 'dark') + }) + test.afterEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.ColorPalette', 'dark') + }) + test('Responds to a change in theme', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('default') + await comfyPage.page.locator('.documentationSidebar-tab-button').click() + const docPane = comfyPage.page.locator('.sidebar-content-container') + await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await expect(docPane).toHaveScreenshot( + 'documentation-sidebar-light-theme.png' + ) + }) + }) +}) diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index aa640d835..e710fe847 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -1,8 +1,10 @@ import { app } from '../../scripts/app' -import { api } from '../../scripts/api' import { getColorPalette } from './colorPalette' +import type { CustomSidebarTabExtension } from '../../types/extensionTypes' +import { LiteGraph } from '@comfyorg/litegraph' var helpDOM = document.createElement('div') +var cdef function setCollapse(el, doCollapse) { if (doCollapse) { @@ -36,16 +38,16 @@ function setCollapse(el, doCollapse) { } } } -helpDOM.collapseOnClick = function () { +function collapseOnClick() { let doCollapse = this.children[0].innerHTML == '-' setCollapse(this.parentElement, doCollapse) } //TODO: connect with doc tooltips //If doc sidebar is opened, the current node should not display tooltips, //but navigate the sidebar pane as appropriate. -helpDOM.selectHelp = function (name: string, value?: string) { - if (helpDOM.def[2]?.select) { - return helpDOM.def[2].select(this, name, value) +function selectHelp(name: string, value?: string) { + if (cdef[2]?.select) { + return cdef[2].select(this, name, value) } //attempt to navigate to name in help function collapseUnlessMatch(items, t) { @@ -86,24 +88,24 @@ helpDOM.selectHelp = function (name: string, value?: string) { } } app.tooltipCallback = function (node, name, value) { - if (node != app.graph._nodes[app.graph._nodes.length - 1]) { + if (node != app.canvas.current_node) { return false } if (name == 'DESCRIPTION') { return false } - helpDOM.selectHelp(name, value) + selectHelp(name, value) return true } -function updateNode(node) { +function updateNode(node?) { //Always use latest node. If it lacks documentation, that should be communicated //instead of confusing users by picking a different recent node that does - node ||= app.graph._nodes[app.graph._nodes.length - 1] + node ||= app.canvas.current_node const def = LiteGraph.getNodeType(node.type).nodeData - if (helpDOM.def == def) { + if (cdef == def) { return } - helpDOM.def = def + cdef = def if (Array.isArray(def.description)) { helpDOM.innerHTML = def.description[1] } else { @@ -166,12 +168,12 @@ docStyleElement.innerHTML = documentationStyle document.body.append(docStyleElement) var bringToFront -let documentationSidebar = { - id: 'documentationSidebar', - title: 'Documentation', - icon: 'DocumentationIcon', - type: 'custom', - render: (e) => { +class DocumentationSidebar implements CustomSidebarTabExtension { + id = 'documentationSidebar' + title = 'Documentation' + type + icon = 'DocumentationIcon' + render(e) { if (!bringToFront) { var bringToFront = app.canvas.bringToFront app.canvas.bringToFront = function (node) { @@ -207,9 +209,9 @@ let documentationSidebar = { if (!e?.children?.length) { e.appendChild(helpDOM) } - if (helpDOM.def.description[2]?.render) { - helpDOM.def.description[2].render(e) + if (cdef.description[2]?.render) { + cdef.description[2].render(e) } } } -app.extensionManager.registerSidebarTab(documentationSidebar) +app.extensionManager.registerSidebarTab(new DocumentationSidebar()) diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 2619484a6..9992dbbd6 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -120,6 +120,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 From 52933e13f598b9ad30f46b79980e8e798499ba44 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Thu, 3 Oct 2024 16:58:37 -0500 Subject: [PATCH 09/24] Properly handle theme with css variables --- src/extensions/core/documentationSidebar.ts | 48 +++++++++------------ 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index e710fe847..420c2a084 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -159,13 +159,29 @@ function updateNode(node?) { } } let docStyleElement = document.createElement('style') -let documentationStyle = ` +docStyleElement.innerHTML = ` +.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; +} .DocumentationIcon:before { - font-size: 1.5em; content: '?'; + font-size: 1.5em; content: '?'; } ` -docStyleElement.innerHTML = documentationStyle -document.body.append(docStyleElement) +document.querySelector('.side-tool-bar-container').append(docStyleElement) var bringToFront class DocumentationSidebar implements CustomSidebarTabExtension { @@ -181,30 +197,6 @@ class DocumentationSidebar implements CustomSidebarTabExtension { return bringToFront.apply(this, arguments) } } - //TODO: properly update colors when theme is toggled - let documentationStyle = ` - .doc-node { - font-size: 1.5em - } - .doc-section { - background-color: ${getColorPalette().colors.comfy_base['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; - } - .DocumentationIcon:before { - font-size: 1.5em; content: '?'; - } - ` - docStyleElement.innerHTML = documentationStyle updateNode() if (!e?.children?.length) { e.appendChild(helpDOM) From 3b679f11949f0b40be06afac0d71761dbad24b0d Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Thu, 3 Oct 2024 17:33:18 -0500 Subject: [PATCH 10/24] 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. --- browser_tests/documentationSidebar.spec.ts | 39 +++++++++++---------- src/extensions/core/documentationSidebar.ts | 2 +- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/browser_tests/documentationSidebar.spec.ts b/browser_tests/documentationSidebar.spec.ts index b2c51865f..e3f6b9048 100644 --- a/browser_tests/documentationSidebar.spec.ts +++ b/browser_tests/documentationSidebar.spec.ts @@ -2,13 +2,25 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from './ComfyPage' 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('.documentationSidebar-tab-button') ).toBeVisible() }) test('Parses help for basic node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') await comfyPage.page.locator('.documentationSidebar-tab-button').click() const docPane = comfyPage.page.locator('.sidebar-content-container') //Check that each independently parsed element exists @@ -18,7 +30,6 @@ test.describe('Documentation Sidebar', () => { await expect(docPane).toContainText('The VAE model used') }) test('Responds to hovering over node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') await comfyPage.page.locator('.documentationSidebar-tab-button').click() const docPane = comfyPage.page.locator('.sidebar-content-container') await comfyPage.page.mouse.move(321, 593) @@ -32,7 +43,6 @@ test.describe('Documentation Sidebar', () => { ).toBeFocused() }) test('Updates when a new node is selected', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') await comfyPage.page.locator('.documentationSidebar-tab-button').click() const docPane = comfyPage.page.locator('.sidebar-content-container') await comfyPage.page.mouse.click(557, 440) @@ -43,21 +53,12 @@ test.describe('Documentation Sidebar', () => { 'A conditioning containing the embedded text' ) }) - test.describe('Theming', () => { - test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ColorPalette', 'dark') - }) - test.afterEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ColorPalette', 'dark') - }) - test('Responds to a change in theme', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') - await comfyPage.page.locator('.documentationSidebar-tab-button').click() - const docPane = comfyPage.page.locator('.sidebar-content-container') - await comfyPage.setSetting('Comfy.ColorPalette', 'light') - await expect(docPane).toHaveScreenshot( - 'documentation-sidebar-light-theme.png' - ) - }) + test('Responds to a change in theme', async ({ comfyPage }) => { + await comfyPage.page.locator('.documentationSidebar-tab-button').click() + const docPane = comfyPage.page.locator('.sidebar-content-container') + comfyPage.menu.toggleTheme() + await expect(docPane).toHaveScreenshot( + 'documentation-sidebar-light-theme.png' + ) }) }) diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index 420c2a084..2fb13aa59 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -181,7 +181,7 @@ docStyleElement.innerHTML = ` font-size: 1.5em; content: '?'; } ` -document.querySelector('.side-tool-bar-container').append(docStyleElement) +document.body.append(docStyleElement) var bringToFront class DocumentationSidebar implements CustomSidebarTabExtension { From bc6630742b32fe44083cf597678c6a8e86e56428 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Thu, 3 Oct 2024 18:26:58 -0500 Subject: [PATCH 11/24] Move render callback to trigger on node change --- src/extensions/core/documentationSidebar.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index 2fb13aa59..81ebb0c1e 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -42,9 +42,6 @@ function collapseOnClick() { let doCollapse = this.children[0].innerHTML == '-' setCollapse(this.parentElement, doCollapse) } -//TODO: connect with doc tooltips -//If doc sidebar is opened, the current node should not display tooltips, -//but navigate the sidebar pane as appropriate. function selectHelp(name: string, value?: string) { if (cdef[2]?.select) { return cdef[2].select(this, name, value) @@ -156,6 +153,9 @@ function updateNode(node?) { } content = '
' + def.display_name + '
' + content helpDOM.innerHTML = content + if (cdef.description[2]?.render) { + cdef.description[2].render(helpDOM) + } } } let docStyleElement = document.createElement('style') @@ -201,9 +201,6 @@ class DocumentationSidebar implements CustomSidebarTabExtension { if (!e?.children?.length) { e.appendChild(helpDOM) } - if (cdef.description[2]?.render) { - cdef.description[2].render(e) - } } } app.extensionManager.registerSidebarTab(new DocumentationSidebar()) From 95cec85c3f1afe70ea12fb1c0ed45d3d9b0170ea Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Fri, 4 Oct 2024 13:42:16 -0500 Subject: [PATCH 12/24] Move css to style.css Since the the css is now static the clutter of an added style element is no longer needed --- src/assets/css/style.css | 21 ++++++++++++++++++ src/extensions/core/documentationSidebar.ts | 24 --------------------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 1e9596df4..d7de7d645 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -715,3 +715,24 @@ audio.comfy-audio.empty-audio-widget { .p-tree-node-content { padding: var(--comfy-tree-explorer-item-padding) !important; } + +.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; +} +.DocumentationIcon:before { + font-size: 1.5em; content: '?'; +} diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts index 81ebb0c1e..9a304d559 100644 --- a/src/extensions/core/documentationSidebar.ts +++ b/src/extensions/core/documentationSidebar.ts @@ -158,30 +158,6 @@ function updateNode(node?) { } } } -let docStyleElement = document.createElement('style') -docStyleElement.innerHTML = ` -.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; -} -.DocumentationIcon:before { - font-size: 1.5em; content: '?'; -} -` -document.body.append(docStyleElement) var bringToFront class DocumentationSidebar implements CustomSidebarTabExtension { From 95a4fe7e08da13a27f086f7994dd647948851315 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 7 Oct 2024 15:28:06 -0500 Subject: [PATCH 13/24] Port sidebar documentation to vue component --- src/assets/css/style.css | 21 -- .../sidebar/tabs/DocumentationSidebarTab.vue | 206 ++++++++++++++++++ src/extensions/core/documentationSidebar.ts | 182 ---------------- src/extensions/core/index.ts | 1 - 4 files changed, 206 insertions(+), 204 deletions(-) create mode 100644 src/components/sidebar/tabs/DocumentationSidebarTab.vue delete mode 100644 src/extensions/core/documentationSidebar.ts diff --git a/src/assets/css/style.css b/src/assets/css/style.css index d7de7d645..1e9596df4 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -715,24 +715,3 @@ audio.comfy-audio.empty-audio-widget { .p-tree-node-content { padding: var(--comfy-tree-explorer-item-padding) !important; } - -.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; -} -.DocumentationIcon:before { - font-size: 1.5em; content: '?'; -} diff --git a/src/components/sidebar/tabs/DocumentationSidebarTab.vue b/src/components/sidebar/tabs/DocumentationSidebarTab.vue new file mode 100644 index 000000000..a92f044dd --- /dev/null +++ b/src/components/sidebar/tabs/DocumentationSidebarTab.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/src/extensions/core/documentationSidebar.ts b/src/extensions/core/documentationSidebar.ts deleted file mode 100644 index 9a304d559..000000000 --- a/src/extensions/core/documentationSidebar.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { app } from '../../scripts/app' -import { getColorPalette } from './colorPalette' -import type { CustomSidebarTabExtension } from '../../types/extensionTypes' -import { LiteGraph } from '@comfyorg/litegraph' - -var helpDOM = document.createElement('div') -var cdef - -function setCollapse(el, doCollapse) { - if (doCollapse) { - el.children[0].children[0].innerHTML = '+' - Object.assign(el.children[1].style, { - color: '#CCC', - overflowX: 'hidden', - width: '0px', - minWidth: 'calc(100% - 20px)', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap' - }) - for (let child of el.children[1].children) { - if (child.style.display != 'none') { - child.origDisplay = child.style.display - } - child.style.display = 'none' - } - } else { - el.children[0].children[0].innerHTML = '-' - Object.assign(el.children[1].style, { - color: '', - overflowX: '', - width: '100%', - minWidth: '', - textOverflow: '', - whiteSpace: '' - }) - for (let child of el.children[1].children) { - child.style.display = child.origDisplay - } - } -} -function collapseOnClick() { - let doCollapse = this.children[0].innerHTML == '-' - setCollapse(this.parentElement, doCollapse) -} -function selectHelp(name: string, value?: string) { - if (cdef[2]?.select) { - return cdef[2].select(this, 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(helpDOM, name) - if (target) { - target.focus() - if (value) { - collapseUnlessMatch(target, value) - } - } -} -app.tooltipCallback = function (node, name, value) { - if (node != app.canvas.current_node) { - return false - } - if (name == 'DESCRIPTION') { - return false - } - selectHelp(name, value) - return true -} -function updateNode(node?) { - //Always use latest node. If it lacks documentation, that should be communicated - //instead of confusing users by picking a different recent node that does - node ||= app.canvas.current_node - const def = LiteGraph.getNodeType(node.type).nodeData - if (cdef == def) { - return - } - cdef = def - if (Array.isArray(def.description)) { - helpDOM.innerHTML = def.description[1] - } else { - //do additional parsing to prettify output and combine tooltips - let content = '' - if (def.description) { - content += '
' + def.description + '
' - } - let inputs = [] - for (let input in def?.input?.required || {}) { - if (def.input.required[input][1]?.tooltip) { - inputs.push([input, def.input.required[input][1].tooltip]) - } - } - for (let input in def?.input?.optional || {}) { - if (def.input.optional[input][1]?.tooltip) { - inputs.push([input, def.input.optional[input][1].tooltip]) - } - } - if (inputs.length) { - content += '
Inputs
' - for (let [k, v] of inputs) { - content += - '
' + - k + - '
' + - v + - '
' - } - //content += "
" - //content += '

' + inputs.join('
') + '
' - } - if (def.output_tooltips) { - content += '
Outputs
' - let outputs = def.output_name || def.output - for (let i = 0; i < outputs.length; i++) { - content += - '
' + - outputs[i] + - '
' + - def.output_tooltips[i] + - '
' - } - //outputs += '
' - } - if (content == '') { - content = 'No documentation available' - } - content = '
' + def.display_name + '
' + content - helpDOM.innerHTML = content - if (cdef.description[2]?.render) { - cdef.description[2].render(helpDOM) - } - } -} - -var bringToFront -class DocumentationSidebar implements CustomSidebarTabExtension { - id = 'documentationSidebar' - title = 'Documentation' - type - icon = 'DocumentationIcon' - render(e) { - if (!bringToFront) { - var bringToFront = app.canvas.bringToFront - app.canvas.bringToFront = function (node) { - updateNode(node) - return bringToFront.apply(this, arguments) - } - } - updateNode() - if (!e?.children?.length) { - e.appendChild(helpDOM) - } - } -} -app.extensionManager.registerSidebarTab(new DocumentationSidebar()) diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index 304a11d60..772dd5f3d 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -22,4 +22,3 @@ import './webcamCapture' import './widgetInputs' import './uploadAudio' import './nodeBadge' -import './documentationSidebar' From b2ef66e0581e28d1c47831f595d9fdbb436cf2d5 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 8 Oct 2024 11:55:43 -0500 Subject: [PATCH 14/24] Update tooltip handling --- src/components/graph/NodeTooltip.vue | 9 ++-- .../sidebar/tabs/DocumentationSidebarTab.vue | 51 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/components/graph/NodeTooltip.vue b/src/components/graph/NodeTooltip.vue index fd3015bdd..5a3b97d9f 100644 --- a/src/components/graph/NodeTooltip.vue +++ b/src/components/graph/NodeTooltip.vue @@ -15,6 +15,7 @@ import { LiteGraph } from '@comfyorg/litegraph' import { app as comfyApp } from '@/scripts/app' import { useNodeDefStore } from '@/stores/nodeDefStore' import { useEventListener } from '@vueuse/core' +import { selectDocItem } from '@/components/sidebar/tabs/DocumentationSidebarTab.vue' let idleTimeout: number const nodeDefStore = useNodeDefStore() @@ -85,7 +86,7 @@ const onIdle = () => { 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 ) { - if (comfyApp?.tooltipCallback?.(node, 'DESCRIPTION')) { + if (selectDocItem(node, 'DESCRIPTION')) { return } else if (Array.isArray(nodeDef.description)) { return showTooltip(nodeDef.description[0]) @@ -103,7 +104,7 @@ const onIdle = () => { ) if (inputSlot !== -1) { const inputName = node.inputs[inputSlot].name - if (!comfyApp?.tooltipCallback?.(node, inputName)) { + if (selectDocItem(node, inputName)) { return showTooltip(nodeDef.input.getInput(inputName)?.tooltip) } } @@ -116,7 +117,7 @@ const onIdle = () => { ) if (outputSlot !== -1) { const outputDef = nodeDef.output.all?.[outputSlot] - if (!comfyApp?.tooltipCallback?.(node, outputDef?.name)) { + if (selectDocItem(node, outputDef?.name)) { return showTooltip(outputDef?.tooltip) } } @@ -124,7 +125,7 @@ const onIdle = () => { const widget = getHoveredWidget() // Dont show for DOM widgets, these use native browser tooltips as we dont get proper mouse events on these if (widget && !widget.element) { - if (!comfyApp?.tooltipCallback?.(node, widget.name, widget.value)) { + if (selectDocItem(node, widget.name, widget.value)) { return showTooltip( widget.tooltip ?? nodeDef.input.getInput(widget.name)?.tooltip ) diff --git a/src/components/sidebar/tabs/DocumentationSidebarTab.vue b/src/components/sidebar/tabs/DocumentationSidebarTab.vue index a92f044dd..649044a05 100644 --- a/src/components/sidebar/tabs/DocumentationSidebarTab.vue +++ b/src/components/sidebar/tabs/DocumentationSidebarTab.vue @@ -27,22 +27,24 @@ - - diff --git a/src/hooks/sidebarTabs/documentationSidebarTab.ts b/src/hooks/sidebarTabs/documentationSidebarTab.ts index 3daf0d6cf..87356c128 100644 --- a/src/hooks/sidebarTabs/documentationSidebarTab.ts +++ b/src/hooks/sidebarTabs/documentationSidebarTab.ts @@ -9,7 +9,7 @@ export const useDocumentationSidebarTab = (): SidebarTabExtension => { const queuePendingTaskCountStore = useQueuePendingTaskCountStore() return { id: 'documentation', - icon: 'pi pi-question', + icon: 'mdi mdi-help', title: t('sideToolbar.documentation'), tooltip: t('sideToolbar.documentation'), component: markRaw(DocumentationSidebarTab), From 92633303791e95ef3b252378e3193c97c9490295 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Thu, 10 Oct 2024 12:18:12 -0500 Subject: [PATCH 17/24] Update tests for vue port Mostly minor changes to selectors Also fixes the glaringly obvious omission of the description field --- browser_tests/documentationSidebar.spec.ts | 14 ++++++-------- .../sidebar/tabs/DocumentationSidebarTab.vue | 13 ++++++++++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/browser_tests/documentationSidebar.spec.ts b/browser_tests/documentationSidebar.spec.ts index e3f6b9048..de4b8fcaa 100644 --- a/browser_tests/documentationSidebar.spec.ts +++ b/browser_tests/documentationSidebar.spec.ts @@ -17,11 +17,11 @@ test.describe('Documentation Sidebar', () => { test('Sidebar registered', async ({ comfyPage }) => { await expect( - comfyPage.page.locator('.documentationSidebar-tab-button') + comfyPage.page.locator('.documentation-tab-button') ).toBeVisible() }) test('Parses help for basic node', async ({ comfyPage }) => { - await comfyPage.page.locator('.documentationSidebar-tab-button').click() + 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') @@ -30,20 +30,18 @@ test.describe('Documentation Sidebar', () => { await expect(docPane).toContainText('The VAE model used') }) test('Responds to hovering over node', async ({ comfyPage }) => { - await comfyPage.page.locator('.documentationSidebar-tab-button').click() + 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( - '.side-bar-panel > div > div > div > div:nth-child(4)' - ) + 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('.documentationSidebar-tab-button').click() + 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') @@ -54,7 +52,7 @@ test.describe('Documentation Sidebar', () => { ) }) test('Responds to a change in theme', async ({ comfyPage }) => { - await comfyPage.page.locator('.documentationSidebar-tab-button').click() + await comfyPage.page.locator('.documentation-tab-button').click() const docPane = comfyPage.page.locator('.sidebar-content-container') comfyPage.menu.toggleTheme() await expect(docPane).toHaveScreenshot( diff --git a/src/components/sidebar/tabs/DocumentationSidebarTab.vue b/src/components/sidebar/tabs/DocumentationSidebarTab.vue index e6305771f..829adbe76 100644 --- a/src/components/sidebar/tabs/DocumentationSidebarTab.vue +++ b/src/components/sidebar/tabs/DocumentationSidebarTab.vue @@ -3,6 +3,7 @@
{{ title }}
+
{{ description }}
Inputs
canvasStore?.canvas?.current_node, updateNode) updateNode() - return { hasAnyDoc, inputs, outputs, def, docElement, title, rawDoc } + return { + hasAnyDoc, + inputs, + outputs, + docElement, + title, + rawDoc, + description + } } } From f48594fbd51e41eeb02a7d6b99178c74cb143d23 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 15 Oct 2024 13:41:09 -0500 Subject: [PATCH 18/24] Properly mirror the new description type Remove errant logging --- src/components/sidebar/tabs/DocumentationSidebarTab.vue | 1 - src/stores/nodeDefStore.ts | 5 +++-- src/types/apiTypes.ts | 1 + src/types/comfy.d.ts | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/sidebar/tabs/DocumentationSidebarTab.vue b/src/components/sidebar/tabs/DocumentationSidebarTab.vue index 829adbe76..d82023ebc 100644 --- a/src/components/sidebar/tabs/DocumentationSidebarTab.vue +++ b/src/components/sidebar/tabs/DocumentationSidebarTab.vue @@ -84,7 +84,6 @@ function collapseOnClick() { } function selectHelp(name: string, value?: string) { if (!docElement.value) { - console.log("doc element doesn't exist") return null } if (def[2]?.select) { diff --git a/src/stores/nodeDefStore.ts b/src/stores/nodeDefStore.ts index 4256f2a68..e8eb8cea1 100644 --- a/src/stores/nodeDefStore.ts +++ b/src/stores/nodeDefStore.ts @@ -5,7 +5,8 @@ import { } from '@/services/nodeSearchService' import { type ComfyNodeDef, - type ComfyInputsSpec as ComfyInputsSpecSchema + type ComfyInputsSpec as ComfyInputsSpecSchema, + type DescriptionSpec } from '@/types/apiTypes' import { defineStore } from 'pinia' import { ComfyWidgetConstructor } from '@/scripts/widgets' @@ -154,7 +155,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/types/apiTypes.ts b/src/types/apiTypes.ts index f3c5710d9..ca4c4a117 100644 --- a/src/types/apiTypes.ts +++ b/src/types/apiTypes.ts @@ -372,6 +372,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.d.ts b/src/types/comfy.d.ts index db9b8eb60..f9a0d3d5e 100644 --- a/src/types/comfy.d.ts +++ b/src/types/comfy.d.ts @@ -1,6 +1,6 @@ import { LGraphNode, IWidget } from './litegraph' import { 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' @@ -117,7 +117,7 @@ export interface ComfyExtension { export type ComfyObjectInfo = { name: string display_name?: string - description?: [string | string, string | string, string, Record] + description?: DescriptionSpec category: string input?: { required?: Record From 1dfcc7a0d448be1f7788b951420d455d9717dcbf Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Fri, 18 Oct 2024 18:56:53 -0500 Subject: [PATCH 19/24] 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. --- src/components/graph/NodeTooltip.vue | 56 +++++++++++------- .../sidebar/tabs/DocumentationSidebarTab.vue | 58 +++++++++++-------- src/stores/graphStore.ts | 9 +++ src/types/comfy.d.ts | 6 ++ 4 files changed, 82 insertions(+), 47 deletions(-) diff --git a/src/components/graph/NodeTooltip.vue b/src/components/graph/NodeTooltip.vue index 3089866a2..3d3d3d435 100644 --- a/src/components/graph/NodeTooltip.vue +++ b/src/components/graph/NodeTooltip.vue @@ -10,15 +10,16 @@