mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-27 01:39:47 +00:00
Compare commits
25 Commits
feat/story
...
sidebar-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
867c50f7cb | ||
|
|
89027ea969 | ||
|
|
ce9cfdb975 | ||
|
|
ef191033c8 | ||
|
|
293f4295a8 | ||
|
|
3252d62edf | ||
|
|
1dfcc7a0d4 | ||
|
|
f48594fbd5 | ||
|
|
9263330379 | ||
|
|
214f48a6c4 | ||
|
|
f8ba0ab24f | ||
|
|
b2ef66e058 | ||
|
|
95a4fe7e08 | ||
|
|
95cec85c3f | ||
|
|
bc6630742b | ||
|
|
3b679f1194 | ||
|
|
52933e13f5 | ||
|
|
44f900ef56 | ||
|
|
7a5d39f41f | ||
|
|
1d0ae76f8c | ||
|
|
a8ac7296c2 | ||
|
|
4aa04d1419 | ||
|
|
8160ca0342 | ||
|
|
da936d69b6 | ||
|
|
7eaa54fe3f |
125
browser_tests/documentationSidebar.spec.ts
Normal file
125
browser_tests/documentationSidebar.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { expect } from '@playwright/test'
|
||||||
|
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||||
|
const nodeDef = {
|
||||||
|
title: 'TestNodeAdvancedDoc'
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Documentation Sidebar', () => {
|
||||||
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('default')
|
||||||
|
})
|
||||||
|
|
||||||
|
test.afterEach(async ({ comfyPage }) => {
|
||||||
|
const currentThemeId = await comfyPage.menu.getThemeId()
|
||||||
|
if (currentThemeId !== 'dark') {
|
||||||
|
await comfyPage.menu.toggleTheme()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Sidebar registered', async ({ comfyPage }) => {
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.locator('.documentation-tab-button')
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
test('Parses help for basic node', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||||
|
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||||
|
//Check that each independently parsed element exists
|
||||||
|
await expect(docPane).toContainText('Load Checkpoint')
|
||||||
|
await expect(docPane).toContainText('Loads a diffusion model')
|
||||||
|
await expect(docPane).toContainText('The name of the checkpoint')
|
||||||
|
await expect(docPane).toContainText('The VAE model used')
|
||||||
|
})
|
||||||
|
test('Responds to hovering over node', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||||
|
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||||
|
await comfyPage.page.mouse.move(321, 593)
|
||||||
|
const tooltipTimeout = 500
|
||||||
|
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
|
||||||
|
await expect(comfyPage.page.locator('.node-tooltip')).not.toBeVisible()
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.locator('.sidebar-content-container>div>div:nth-child(4)')
|
||||||
|
).toBeFocused()
|
||||||
|
})
|
||||||
|
test('Updates when a new node is selected', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||||
|
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||||
|
await comfyPage.page.mouse.click(557, 440)
|
||||||
|
await expect(docPane).not.toContainText('Load Checkpoint')
|
||||||
|
await expect(docPane).toContainText('CLIP Text Encode (Prompt)')
|
||||||
|
await expect(docPane).toContainText('The text to be encoded')
|
||||||
|
await expect(docPane).toContainText(
|
||||||
|
'A conditioning containing the embedded text'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
test('Responds to a change in theme', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||||
|
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||||
|
await comfyPage.menu.toggleTheme()
|
||||||
|
await expect(docPane).toHaveScreenshot(
|
||||||
|
'documentation-sidebar-light-theme.png'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test.describe('Advanced Description tests', () => {
|
||||||
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
//register test node and add to graph
|
||||||
|
await comfyPage.page.evaluate(async (node) => {
|
||||||
|
const app = window['app']
|
||||||
|
await app.registerNodeDef(node.name, node)
|
||||||
|
app.addNodeOnGraph(node)
|
||||||
|
}, advDocNode)
|
||||||
|
})
|
||||||
|
test('Description displays as raw html', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||||
|
const docPane = comfyPage.page.locator('.sidebar-content-container>div')
|
||||||
|
await expect(docPane).toHaveJSProperty(
|
||||||
|
'innerHTML',
|
||||||
|
advDocNode.description[1]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
test('selected function', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.page.evaluate(() => {
|
||||||
|
const app = window['app']
|
||||||
|
const desc =
|
||||||
|
LiteGraph.registered_node_types['Test_AdvancedDescription'].nodeData
|
||||||
|
.description
|
||||||
|
desc[2].select = (element, name, value) => {
|
||||||
|
element.children[0].innerText = name + ' ' + value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await comfyPage.page.locator('.documentation-tab-button').click()
|
||||||
|
const docPane = comfyPage.page.locator('.sidebar-content-container')
|
||||||
|
await comfyPage.page.mouse.move(307, 80)
|
||||||
|
const tooltipTimeout = 500
|
||||||
|
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
|
||||||
|
await expect(comfyPage.page.locator('.node-tooltip')).not.toBeVisible()
|
||||||
|
await expect(docPane).toContainText('int_input 0')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const advDocNode = {
|
||||||
|
display_name: 'Node With Advanced Description',
|
||||||
|
name: 'Test_AdvancedDescription',
|
||||||
|
input: {
|
||||||
|
required: {
|
||||||
|
int_input: [
|
||||||
|
'INT',
|
||||||
|
{ tooltip: "an input tooltip that won't be displayed in sidebar" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: ['INT'],
|
||||||
|
output_name: ['int_output'],
|
||||||
|
output_tooltips: ["An output tooltip that won't be displayed in the sidebar"],
|
||||||
|
output_is_list: false,
|
||||||
|
description: [
|
||||||
|
'A node with description in the advanced format',
|
||||||
|
`
|
||||||
|
A long form description that will be displayed in the sidebar.
|
||||||
|
<div doc_title="INT">Can include arbitrary html</div>
|
||||||
|
<div doc_title="int_input">or out of order widgets</div>
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -10,20 +10,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, ref } from 'vue'
|
import { nextTick, ref, watch } from 'vue'
|
||||||
import { LiteGraph } from '@comfyorg/litegraph'
|
import { LiteGraph } from '@comfyorg/litegraph'
|
||||||
import { app as comfyApp } from '@/scripts/app'
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
|
import { useHoveredItemStore } from '@/stores/graphStore'
|
||||||
import { useEventListener } from '@vueuse/core'
|
import { useEventListener } from '@vueuse/core'
|
||||||
|
|
||||||
let idleTimeout: number
|
let idleTimeout: number
|
||||||
const nodeDefStore = useNodeDefStore()
|
const nodeDefStore = useNodeDefStore()
|
||||||
|
const hoveredItemStore = useHoveredItemStore()
|
||||||
const tooltipRef = ref<HTMLDivElement>()
|
const tooltipRef = ref<HTMLDivElement>()
|
||||||
const tooltipText = ref('')
|
const tooltipText = ref('')
|
||||||
const left = ref<string>()
|
const left = ref<string>()
|
||||||
const top = ref<string>()
|
const top = ref<string>()
|
||||||
|
|
||||||
const hideTooltip = () => (tooltipText.value = null)
|
const hideTooltip = () => (tooltipText.value = null)
|
||||||
|
const clearHovered = () => (hoveredItemStore.value = null)
|
||||||
|
|
||||||
const showTooltip = async (tooltip: string | null | undefined) => {
|
const showTooltip = async (tooltip: string | null | undefined) => {
|
||||||
if (!tooltip) return
|
if (!tooltip) return
|
||||||
@@ -43,6 +46,36 @@ const showTooltip = async (tooltip: string | null | undefined) => {
|
|||||||
top.value = comfyApp.canvas.mouse[1] + rect.height + 'px'
|
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 onIdle = () => {
|
||||||
const { canvas } = comfyApp
|
const { canvas } = comfyApp
|
||||||
@@ -50,13 +83,15 @@ const onIdle = () => {
|
|||||||
if (!node) return
|
if (!node) return
|
||||||
|
|
||||||
const ctor = node.constructor as { title_mode?: 0 | 1 | 2 | 3 }
|
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 (
|
if (
|
||||||
ctor.title_mode !== LiteGraph.NO_TITLE &&
|
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
|
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
|
if (node.flags?.collapsed) return
|
||||||
@@ -69,7 +104,7 @@ const onIdle = () => {
|
|||||||
)
|
)
|
||||||
if (inputSlot !== -1) {
|
if (inputSlot !== -1) {
|
||||||
const inputName = node.inputs[inputSlot].name
|
const inputName = node.inputs[inputSlot].name
|
||||||
return showTooltip(nodeDef.input.getInput(inputName)?.tooltip)
|
hoveredItemStore.value = { node, type: 'Input', inputName }
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputSlot = canvas.isOverNodeOutput(
|
const outputSlot = canvas.isOverNodeOutput(
|
||||||
@@ -79,20 +114,18 @@ const onIdle = () => {
|
|||||||
[0, 0]
|
[0, 0]
|
||||||
)
|
)
|
||||||
if (outputSlot !== -1) {
|
if (outputSlot !== -1) {
|
||||||
return showTooltip(nodeDef.output.all?.[outputSlot]?.tooltip)
|
hoveredItemStore.value = { node, type: 'Output', outputSlot }
|
||||||
}
|
}
|
||||||
|
|
||||||
const widget = comfyApp.canvas.getWidgetAtCursor()
|
const widget = comfyApp.canvas.getWidgetAtCursor()
|
||||||
// Dont show for DOM widgets, these use native browser tooltips as we dont get proper mouse events on these
|
// Dont show for DOM widgets, these use native browser tooltips as we dont get proper mouse events on these
|
||||||
if (widget && !widget.element) {
|
if (widget && !widget.element) {
|
||||||
return showTooltip(
|
hoveredItemStore.value = { node, type: 'Widget', widget }
|
||||||
widget.tooltip ?? nodeDef.input.getInput(widget.name)?.tooltip
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
hideTooltip()
|
clearHovered()
|
||||||
clearTimeout(idleTimeout)
|
clearTimeout(idleTimeout)
|
||||||
|
|
||||||
if ((e.target as Node).nodeName !== 'CANVAS') return
|
if ((e.target as Node).nodeName !== 'CANVAS') return
|
||||||
@@ -100,7 +133,7 @@ const onMouseMove = (e: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEventListener(window, 'mousemove', onMouseMove)
|
useEventListener(window, 'mousemove', onMouseMove)
|
||||||
useEventListener(window, 'click', hideTooltip)
|
useEventListener(window, 'click', clearHovered)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
|||||||
backgroundColor: litegraphColors.WIDGET_BGCOLOR
|
backgroundColor: litegraphColors.WIDGET_BGCOLOR
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ nodeDef.description }}
|
{{
|
||||||
|
Array.isArray(nodeDef.description)
|
||||||
|
? nodeDef.description[0]
|
||||||
|
: nodeDef.description
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
196
src/components/sidebar/tabs/DocumentationSidebarTab.vue
Normal file
196
src/components/sidebar/tabs/DocumentationSidebarTab.vue
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="!hasAnyDoc()">Select a node to see documentation.</div>
|
||||||
|
<div v-else-if="rawDoc" ref="docElement" v-html="rawDoc"></div>
|
||||||
|
<div v-else ref="docElement">
|
||||||
|
<div class="doc-node">{{ title }}</div>
|
||||||
|
<div>{{ description }}</div>
|
||||||
|
<div v-if="inputs.length" class="doc-section">Inputs</div>
|
||||||
|
<div
|
||||||
|
v-if="inputs.length"
|
||||||
|
v-for="input in inputs"
|
||||||
|
tabindex="-1"
|
||||||
|
class="doc-item"
|
||||||
|
>
|
||||||
|
{{ input[0] }}
|
||||||
|
<div>{{ input[1] }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="outputs.length" class="doc-section">Outputs</div>
|
||||||
|
<div
|
||||||
|
v-if="outputs.length"
|
||||||
|
v-for="output in outputs"
|
||||||
|
tabindex="-1"
|
||||||
|
class="doc-item"
|
||||||
|
>
|
||||||
|
{{ output[0] }}
|
||||||
|
<div>{{ output[1] }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onBeforeUnmount, isReactive } from 'vue'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
import { useHoveredItemStore } from '@/stores/graphStore'
|
||||||
|
const hoveredItemStore = useHoveredItemStore()
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
|
const docElement = ref(null)
|
||||||
|
|
||||||
|
let def
|
||||||
|
const rawDoc = ref(null)
|
||||||
|
const description = ref(null)
|
||||||
|
const title = ref(null)
|
||||||
|
const inputs = ref([])
|
||||||
|
const outputs = ref([])
|
||||||
|
|
||||||
|
function selectHelp(name: string, value?: string) {
|
||||||
|
if (!docElement.value || !name) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (def.description[2]?.select) {
|
||||||
|
return def.description[2].select(docElement.value, name, value)
|
||||||
|
}
|
||||||
|
//attempt to navigate to name in help
|
||||||
|
function collapseUnlessMatch(items, t) {
|
||||||
|
var match = items.querySelector('[doc_title="' + t + '"]')
|
||||||
|
if (!match) {
|
||||||
|
for (let i of items.children) {
|
||||||
|
if (i.innerHTML.slice(0, t.length + 5).includes(t)) {
|
||||||
|
match = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
//For longer documentation items with fewer collapsable elements,
|
||||||
|
//scroll to make sure the entirety of the selected item is visible
|
||||||
|
match.scrollIntoView({ block: 'nearest' })
|
||||||
|
//The previous floating help implementation would try to scroll the window
|
||||||
|
//itself if the display was partiall offscreen. As the sidebar documentation
|
||||||
|
//does not pan with the canvas, this should no longer be needed
|
||||||
|
//window.scrollTo(0, 0)
|
||||||
|
for (let i of items.querySelectorAll('.doc_collapse')) {
|
||||||
|
if (i.contains(match)) {
|
||||||
|
setCollapse(i, false)
|
||||||
|
} else {
|
||||||
|
setCollapse(i, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
let target = collapseUnlessMatch(docElement.value, name)
|
||||||
|
if (target) {
|
||||||
|
target.focus()
|
||||||
|
if (value) {
|
||||||
|
collapseUnlessMatch(target, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateNode() {
|
||||||
|
//Grab the topmost node.
|
||||||
|
//current_node is topmost on screen and
|
||||||
|
//selectedItems is unordered
|
||||||
|
const node = app?.graph?._nodes[app?.graph?._nodes.length - 1]
|
||||||
|
if (!node) {
|
||||||
|
// Graph has no nodes
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nodeDef = LiteGraph.getNodeType(node.type).nodeData
|
||||||
|
if (def == nodeDef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
def = nodeDef
|
||||||
|
title.value = def.display_name
|
||||||
|
if (Array.isArray(def.description)) {
|
||||||
|
rawDoc.value = def.description[1]
|
||||||
|
outputs.value = []
|
||||||
|
inputs.value = []
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rawDoc.value = null
|
||||||
|
}
|
||||||
|
description.value = def.description
|
||||||
|
let input_temp = []
|
||||||
|
for (let k in def?.input?.required) {
|
||||||
|
if (def.input.required[k][1]?.tooltip) {
|
||||||
|
input_temp.push([k, def.input.required[k][1].tooltip])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let k in def?.optional?.required) {
|
||||||
|
if (def.input.optional[k][1]?.tooltip) {
|
||||||
|
input_temp.push([k, def.input.optional[k][1].tooltip])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputs.value = input_temp
|
||||||
|
if (def.output_tooltips) {
|
||||||
|
const outputs_temp = []
|
||||||
|
const output_name = def.output_name || def.output
|
||||||
|
for (let i = 0; i < def.output_tooltips.length; i++) {
|
||||||
|
outputs_temp[i] = [output_name[i], def.output_tooltips[i]]
|
||||||
|
}
|
||||||
|
outputs.value = outputs_temp
|
||||||
|
} else {
|
||||||
|
outputs.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hasAnyDoc() {
|
||||||
|
return def?.description || inputs.value.length || outputs.value.length
|
||||||
|
}
|
||||||
|
watch(hoveredItemStore, (hoveredItem) => {
|
||||||
|
if (!hoveredItem.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const item = hoveredItem.value
|
||||||
|
const nodeDef = LiteGraph.getNodeType(item.node.type).nodeData
|
||||||
|
if (nodeDef != def) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.type == 'DESCRIPTION') {
|
||||||
|
return
|
||||||
|
} else if (item.type == 'Input') {
|
||||||
|
selectHelp(item.inputName)
|
||||||
|
hoveredItem.value = null
|
||||||
|
} else if (item.type == 'Output') {
|
||||||
|
selectHelp(nodeDef?.output?.all?.[item.outputSlot]?.name)
|
||||||
|
hoveredItem.value = null
|
||||||
|
} else if (item.type == 'Widget') {
|
||||||
|
selectHelp(item.widget.name, item.widget.value)
|
||||||
|
hoveredItem.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (isReactive(canvasStore?.canvas)) {
|
||||||
|
watch(() => canvasStore.canvas?.current_node, updateNode)
|
||||||
|
} else {
|
||||||
|
let interval = setInterval(updateNode, 300)
|
||||||
|
onBeforeUnmount(() => clearInterval(this.interval))
|
||||||
|
}
|
||||||
|
updateNode()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.doc-node {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
.doc-section {
|
||||||
|
background-color: var(--comfy-menu-bg);
|
||||||
|
}
|
||||||
|
.doc-item div {
|
||||||
|
margin-inline-start: 1vw;
|
||||||
|
}
|
||||||
|
@keyframes selectAnimation {
|
||||||
|
0% {
|
||||||
|
background-color: #5555;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
background-color: #5555;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: #0000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doc-item:focus {
|
||||||
|
animation: selectAnimation 2s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
18
src/hooks/sidebarTabs/documentationSidebarTab.ts
Normal file
18
src/hooks/sidebarTabs/documentationSidebarTab.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useQueuePendingTaskCountStore } from '@/stores/queueStore'
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import DocumentationSidebarTab from '@/components/sidebar/tabs/DocumentationSidebarTab.vue'
|
||||||
|
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||||
|
|
||||||
|
export const useDocumentationSidebarTab = (): SidebarTabExtension => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
|
||||||
|
return {
|
||||||
|
id: 'documentation',
|
||||||
|
icon: 'mdi mdi-help',
|
||||||
|
title: t('sideToolbar.documentation'),
|
||||||
|
tooltip: t('sideToolbar.documentation'),
|
||||||
|
component: markRaw(DocumentationSidebarTab),
|
||||||
|
type: 'vue'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -158,6 +158,7 @@ export default {
|
|||||||
},
|
},
|
||||||
modelLibrary: 'Model Library',
|
modelLibrary: 'Model Library',
|
||||||
downloads: 'Downloads',
|
downloads: 'Downloads',
|
||||||
|
documentation: 'Display documentation for nodes',
|
||||||
queueTab: {
|
queueTab: {
|
||||||
showFlatList: 'Show Flat List',
|
showFlatList: 'Show Flat List',
|
||||||
backToAllTasks: 'Back to All Tasks',
|
backToAllTasks: 'Back to All Tasks',
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ export class ComfyApp {
|
|||||||
canvas: LGraphCanvas
|
canvas: LGraphCanvas
|
||||||
dragOverNode: LGraphNode | null
|
dragOverNode: LGraphNode | null
|
||||||
canvasEl: HTMLCanvasElement
|
canvasEl: HTMLCanvasElement
|
||||||
|
tooltipCallback?: (node: LGraphNode, name: string, value?: string) => boolean
|
||||||
// x, y, scale
|
// x, y, scale
|
||||||
zoom_drag_start: [number, number, number] | null
|
zoom_drag_start: [number, number, number] | null
|
||||||
lastNodeErrors: any[] | null
|
lastNodeErrors: any[] | null
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LGraphNode, LGraphGroup, LGraphCanvas } from '@comfyorg/litegraph'
|
import { LGraphNode, LGraphGroup, LGraphCanvas } from '@comfyorg/litegraph'
|
||||||
|
import type { ComfyNodeItem } from '@/types/comfy'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { shallowRef } from 'vue'
|
import { shallowRef } from 'vue'
|
||||||
|
|
||||||
@@ -10,6 +11,14 @@ export const useTitleEditorStore = defineStore('titleEditor', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const useHoveredItemStore = defineStore('hoveredItem', () => {
|
||||||
|
const hoveredItem = shallowRef<ComfyNodeItem | null>(null)
|
||||||
|
|
||||||
|
return {
|
||||||
|
hoveredItem
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export const useCanvasStore = defineStore('canvas', () => {
|
export const useCanvasStore = defineStore('canvas', () => {
|
||||||
/**
|
/**
|
||||||
* The LGraphCanvas instance.
|
* The LGraphCanvas instance.
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
type ComfyNodeDef,
|
type ComfyNodeDef,
|
||||||
type ComfyInputsSpec as ComfyInputsSpecSchema,
|
type ComfyInputsSpec as ComfyInputsSpecSchema,
|
||||||
type InputSpec
|
type InputSpec,
|
||||||
|
type DescriptionSpec
|
||||||
} from '@/types/apiTypes'
|
} from '@/types/apiTypes'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ComfyWidgetConstructor } from '@/scripts/widgets'
|
import { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||||
@@ -158,7 +159,7 @@ export class ComfyNodeDefImpl {
|
|||||||
display_name: string
|
display_name: string
|
||||||
category: string
|
category: string
|
||||||
python_module: string
|
python_module: string
|
||||||
description: string
|
description: DescriptionSpec
|
||||||
deprecated: boolean
|
deprecated: boolean
|
||||||
experimental: boolean
|
experimental: boolean
|
||||||
input: ComfyInputsSpec
|
input: ComfyInputsSpec
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useModelLibrarySidebarTab } from '@/hooks/sidebarTabs/modelLibrarySideb
|
|||||||
import { useNodeLibrarySidebarTab } from '@/hooks/sidebarTabs/nodeLibrarySidebarTab'
|
import { useNodeLibrarySidebarTab } from '@/hooks/sidebarTabs/nodeLibrarySidebarTab'
|
||||||
import { useQueueSidebarTab } from '@/hooks/sidebarTabs/queueSidebarTab'
|
import { useQueueSidebarTab } from '@/hooks/sidebarTabs/queueSidebarTab'
|
||||||
import { useWorkflowsSidebarTab } from '@/hooks/sidebarTabs/workflowsSidebarTab'
|
import { useWorkflowsSidebarTab } from '@/hooks/sidebarTabs/workflowsSidebarTab'
|
||||||
|
import { useDocumentationSidebarTab } from '@/hooks/sidebarTabs/documentationSidebarTab'
|
||||||
import { SidebarTabExtension } from '@/types/extensionTypes'
|
import { SidebarTabExtension } from '@/types/extensionTypes'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
@@ -57,6 +58,7 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
|||||||
registerSidebarTab(useNodeLibrarySidebarTab())
|
registerSidebarTab(useNodeLibrarySidebarTab())
|
||||||
registerSidebarTab(useModelLibrarySidebarTab())
|
registerSidebarTab(useModelLibrarySidebarTab())
|
||||||
registerSidebarTab(useWorkflowsSidebarTab())
|
registerSidebarTab(useWorkflowsSidebarTab())
|
||||||
|
registerSidebarTab(useDocumentationSidebarTab())
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -356,6 +356,11 @@ const zComfyComboOutput = z.array(z.any())
|
|||||||
const zComfyOutputTypesSpec = z.array(
|
const zComfyOutputTypesSpec = z.array(
|
||||||
z.union([zComfyNodeDataType, zComfyComboOutput])
|
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({
|
const zComfyNodeDef = z.object({
|
||||||
input: zComfyInputsSpec.optional(),
|
input: zComfyInputsSpec.optional(),
|
||||||
@@ -365,7 +370,7 @@ const zComfyNodeDef = z.object({
|
|||||||
output_tooltips: z.array(z.string()).optional(),
|
output_tooltips: z.array(z.string()).optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
display_name: z.string(),
|
display_name: z.string(),
|
||||||
description: z.string(),
|
description: zDescriptionSpec,
|
||||||
category: z.string(),
|
category: z.string(),
|
||||||
output_node: z.boolean(),
|
output_node: z.boolean(),
|
||||||
python_module: z.string(),
|
python_module: z.string(),
|
||||||
@@ -378,6 +383,7 @@ export type InputSpec = z.infer<typeof zInputSpec>
|
|||||||
export type ComfyInputsSpec = z.infer<typeof zComfyInputsSpec>
|
export type ComfyInputsSpec = z.infer<typeof zComfyInputsSpec>
|
||||||
export type ComfyOutputTypesSpec = z.infer<typeof zComfyOutputTypesSpec>
|
export type ComfyOutputTypesSpec = z.infer<typeof zComfyOutputTypesSpec>
|
||||||
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>
|
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>
|
||||||
|
export type DescriptionSpec = z.infer<typeof zDescriptionSpec>
|
||||||
|
|
||||||
export function validateComfyNodeDef(
|
export function validateComfyNodeDef(
|
||||||
data: any,
|
data: any,
|
||||||
|
|||||||
@@ -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 { 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 { Keybinding } from '@/types/keyBindingTypes'
|
||||||
import type { ComfyCommand } from '@/stores/commandStore'
|
import type { ComfyCommand } from '@/stores/commandStore'
|
||||||
import type { SettingParams } from '@/types/settingTypes'
|
import type { SettingParams } from '@/types/settingTypes'
|
||||||
@@ -159,3 +159,9 @@ export interface ComfyExtension {
|
|||||||
|
|
||||||
[key: string]: any
|
[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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user