diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png index 483251f59fa..fd012ac4978 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index 21afb2c038a..f68181d0e83 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index 6b236bd60bd..819ac2fea21 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index eb05d40fc1a..d08cb4a94ff 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index 4dfd064c976..1dacd890360 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index 9c5874347cc..99fd12a3442 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index 47282cee604..dcbc2c535e4 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index d3ad77b7bea..c7b21c0cc66 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png index 625919cec49..009e0eb0d65 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png index ca720c92c40..b576100e497 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png index 4e8263ab2d5..4084d3a252e 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png index cacb943f847..7a6875580fa 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png differ diff --git a/packages/design-system/src/icons/comfy-c.svg b/packages/design-system/src/icons/comfy-c.svg new file mode 100644 index 00000000000..07f62103c09 --- /dev/null +++ b/packages/design-system/src/icons/comfy-c.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/rightSidePanel/RightSidePanel.vue b/src/components/rightSidePanel/RightSidePanel.vue index c99cf71da32..cdf6b891fdd 100644 --- a/src/components/rightSidePanel/RightSidePanel.vue +++ b/src/components/rightSidePanel/RightSidePanel.vue @@ -14,11 +14,13 @@ import { SubgraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import { useSettingStore } from '@/platform/settings/settingStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' +import { useExecutionStore } from '@/stores/executionStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore' import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil' import { cn } from '@/utils/tailwindUtil' +import TabError from './TabError.vue' import TabInfo from './info/TabInfo.vue' import TabGlobalParameters from './parameters/TabGlobalParameters.vue' import TabNodes from './parameters/TabNodes.vue' @@ -33,6 +35,7 @@ import { import SubgraphEditor from './subgraph/SubgraphEditor.vue' const canvasStore = useCanvasStore() +const executionStore = useExecutionStore() const rightSidePanelStore = useRightSidePanelStore() const settingStore = useSettingStore() const { t } = useI18n() @@ -87,10 +90,25 @@ function closePanel() { type RightSidePanelTabList = Array<{ label: () => string value: RightSidePanelTab + icon?: string }> +//FIXME all errors if nothing selected? +const selectedNodeErrors = computed(() => + selectedNodes.value + .map((node) => executionStore.getNodeErrors(`${node.id}`)) + .filter((nodeError) => !!nodeError) +) + const tabs = computed(() => { const list: RightSidePanelTabList = [] + if (selectedNodeErrors.value.length) { + list.push({ + label: () => t('g.error'), + value: 'error', + icon: 'icon-[lucide--octagon-alert] bg-node-stroke-error ml-1' + }) + } list.push({ label: () => @@ -271,6 +289,7 @@ function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) { :value="tab.value" > {{ tab.label() }} + @@ -288,6 +307,7 @@ function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) { :node="selectedSingleNode" /> @@ -172,6 +204,7 @@ import { } from 'vue' import { useI18n } from 'vue-i18n' +import Button from '@/components/ui/button/Button.vue' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu' import { useErrorHandling } from '@/composables/useErrorHandling' @@ -189,10 +222,12 @@ import { useTelemetry } from '@/platform/telemetry' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import NodeBadges from '@/renderer/extensions/vueNodes/components/NodeBadges.vue' import SlotConnectionDot from '@/renderer/extensions/vueNodes/components/SlotConnectionDot.vue' import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers' import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions' import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex' +import { usePartitionedBadges } from '@/renderer/extensions/vueNodes/composables/usePartitionedBadges' import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking' import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState' import { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag' @@ -212,7 +247,6 @@ import { import { cn } from '@/utils/tailwindUtil' import { useNodeResize } from '../interactions/resize/useNodeResize' -import { WidgetInputBaseClass } from '../widgets/components/layout' import LivePreview from './LivePreview.vue' import NodeContent from './NodeContent.vue' import NodeHeader from './NodeHeader.vue' @@ -299,6 +333,7 @@ const { position, size, zIndex } = useNodeLayout(() => nodeData.id) const { pointerHandlers } = useNodePointerInteractions(() => nodeData.id) const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers const { startDrag } = useNodeDrag() +const badges = usePartitionedBadges(nodeData) async function nodeOnPointerdown(event: PointerEvent) { if (event.altKey && lgraphNode.value) { @@ -405,7 +440,7 @@ const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState( ) const borderClass = computed(() => { - if (hasAnyError.value) return 'border-node-stroke-error' + if (hasAnyError.value) return 'border-node-stroke-error bg-error' //FIXME need a better way to detecting transparency if ( !displayHeader.value && diff --git a/src/renderer/extensions/vueNodes/components/NodeBadges.vue b/src/renderer/extensions/vueNodes/components/NodeBadges.vue new file mode 100644 index 00000000000..ac73d78dffc --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/NodeBadges.vue @@ -0,0 +1,55 @@ + + diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.vue b/src/renderer/extensions/vueNodes/components/NodeHeader.vue index 4f9bd75602f..b1600efe32c 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.vue +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.vue @@ -7,20 +7,16 @@ :class=" cn( 'lg-node-header text-sm py-2 pl-2 pr-3 w-full min-w-0', - 'text-node-component-header bg-node-component-header-surface', + 'text-node-component-header', headerShapeClass ) " - :style="{ - backgroundColor: applyLightThemeColor(nodeData?.color), - opacity: useSettingStore().get('Comfy.Node.Opacity') ?? 1 - }" :data-testid="`node-header-${nodeData?.id || ''}`" @dblclick="handleDoubleClick" >
-
+
- -
-
-
-
+
-
- - - - -
+ + + + + + + + +
diff --git a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue index c18db793719..41ad4e93e8d 100644 --- a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue +++ b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue @@ -47,6 +47,7 @@ boundingRect: [0, 0, 0, 0] }" :node-id="nodeData?.id != null ? String(nodeData.id) : ''" + :has-error="widget.hasError" :index="widget.slotMetadata.index" :socketless="widget.simplified.spec?.socketless" dot-only @@ -60,7 +61,12 @@ :widget="widget.simplified" :node-id="nodeData?.id != null ? String(nodeData.id) : ''" :node-type="nodeType" - class="col-span-2" + :class=" + cn( + 'col-span-2', + widget.hasError && 'text-node-stroke-error font-bold' + ) + " @update:model-value="widget.updateHandler" />
@@ -95,6 +101,7 @@ import { stripGraphPrefix, useWidgetValueStore } from '@/stores/widgetValueStore' +import { useExecutionStore } from '@/stores/executionStore' import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget' import { cn } from '@/utils/tailwindUtil' @@ -109,6 +116,7 @@ const { nodeData } = defineProps() const { shouldHandleNodePointerEvents, forwardEventToCanvas } = useCanvasInteractions() const { bringNodeToFront } = useNodeZIndex() +const executionStore = useExecutionStore() function handleWidgetPointerEvent(event: PointerEvent) { if (shouldHandleNodePointerEvents.value) return @@ -146,21 +154,23 @@ const { getWidgetTooltip, createTooltipConfig } = useNodeTooltips( const widgetValueStore = useWidgetValueStore() interface ProcessedWidget { + advanced: boolean + hasLayoutSize: boolean + hasError: boolean + hidden: boolean name: string - type: string - vueComponent: Component simplified: SimplifiedWidget - value: WidgetValue - updateHandler: (value: WidgetValue) => void tooltipConfig: TooltipOptions + type: string + updateHandler: (value: WidgetValue) => void + value: WidgetValue + vueComponent: Component slotMetadata?: WidgetSlotMetadata - hidden: boolean - advanced: boolean - hasLayoutSize: boolean } const processedWidgets = computed((): ProcessedWidget[] => { if (!nodeData?.widgets) return [] + const nodeErrors = executionStore.lastNodeErrors?.[nodeData.id ?? ''] const nodeId = nodeData.id const { widgets } = nodeData @@ -220,6 +230,13 @@ const processedWidgets = computed((): ProcessedWidget[] => { const tooltipConfig = createTooltipConfig(tooltipText) result.push({ + advanced: widget.options?.advanced ?? false, + hasLayoutSize: widget.hasLayoutSize ?? false, + hasError: + nodeErrors?.errors?.some( + (error) => error.extra_info?.input_name === widget.name + ) ?? false, + hidden: widget.options?.hidden ?? false, name: widget.name, type: widget.type, vueComponent, @@ -227,10 +244,7 @@ const processedWidgets = computed((): ProcessedWidget[] => { value, updateHandler, tooltipConfig, - slotMetadata, - hidden: widget.options?.hidden ?? false, - advanced: widget.options?.advanced ?? false, - hasLayoutSize: widget.hasLayoutSize ?? false + slotMetadata }) } diff --git a/src/renderer/extensions/vueNodes/composables/usePartitionedBadges.ts b/src/renderer/extensions/vueNodes/composables/usePartitionedBadges.ts new file mode 100644 index 00000000000..eda1171f144 --- /dev/null +++ b/src/renderer/extensions/vueNodes/composables/usePartitionedBadges.ts @@ -0,0 +1,135 @@ +import { trim } from 'es-toolkit' +import { computed, toValue } from 'vue' + +import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' +import { useNodePricing } from '@/composables/node/useNodePricing' +import { usePriceBadge } from '@/composables/node/usePriceBadge' +import { useSettingStore } from '@/platform/settings/settingStore' +import type { NodeBadgeProps } from '@/renderer/extensions/vueNodes/components/NodeBadge.vue' +import { useWidgetValueStore } from '@/stores/widgetValueStore' +import { useNodeDefStore } from '@/stores/nodeDefStore' +import { NodeBadgeMode } from '@/types/nodeSource' + +function splitAroundFirstSpace(text: string): [string, string | undefined] { + const index = text.indexOf(' ') + if (index === -1) return [text, undefined] + return [text.slice(0, index), text.slice(index + 1)] +} + +export function usePartitionedBadges(nodeData: VueNodeData) { + // Use per-node pricing revision to re-compute badges only when this node's pricing updates + const { + getRelevantWidgetNames, + hasDynamicPricing, + getInputGroupPrefixes, + getInputNames, + getNodeRevisionRef + } = useNodePricing() + + const { isCreditsBadge } = usePriceBadge() + const settingStore = useSettingStore() + + // Cache pricing metadata (won't change during node lifetime) + const isDynamicPricing = computed(() => + nodeData?.apiNode ? hasDynamicPricing(nodeData.type) : false + ) + const relevantPricingWidgets = computed(() => + nodeData?.apiNode ? getRelevantWidgetNames(nodeData.type) : [] + ) + const inputGroupPrefixes = computed(() => + nodeData?.apiNode ? getInputGroupPrefixes(nodeData.type) : [] + ) + const relevantInputNames = computed(() => + nodeData?.apiNode ? getInputNames(nodeData.type) : [] + ) + const unpartitionedBadges = computed(() => { + // For ALL API nodes: access per-node revision ref to detect when async pricing evaluation completes + // This is needed even for static pricing because JSONata 2.x evaluation is async + if (nodeData?.apiNode && nodeData?.id != null) { + // Access per-node revision ref to establish dependency (each node has its own ref) + void getNodeRevisionRef(nodeData.id).value + + // For dynamic pricing, also track widget values and input connections + if (isDynamicPricing.value) { + // Access only the widget values that affect pricing (from widgetValueStore) + const relevantNames = relevantPricingWidgets.value + const widgetStore = useWidgetValueStore() + if (relevantNames.length > 0 && nodeData?.id != null) { + for (const name of relevantNames) { + // Access value from store to create reactive dependency + void widgetStore.getWidget(nodeData.id, name)?.value + } + } + // Access input connections for regular inputs + const inputNames = relevantInputNames.value + if (inputNames.length > 0) { + nodeData?.inputs?.forEach((inp) => { + if (inp.name && inputNames.includes(inp.name)) { + void inp.link // Access link to create reactive dependency + } + }) + } + // Access input connections for input_groups (e.g., autogrow inputs) + const groupPrefixes = inputGroupPrefixes.value + if (groupPrefixes.length > 0) { + nodeData?.inputs?.forEach((inp) => { + if ( + groupPrefixes.some((prefix) => inp.name?.startsWith(prefix + '.')) + ) { + void inp.link // Access link to create reactive dependency + } + }) + } + } + } + return [...(nodeData?.badges ?? [])].map(toValue) + }) + const nodeDef = useNodeDefStore().nodeDefsByName[nodeData.type] + return computed(() => { + const displaySource = settingStore.get( + 'Comfy.NodeBadge.NodeSourceBadgeMode' + ) + const isCoreNode = + nodeDef?.isCoreNode && displaySource === NodeBadgeMode.ShowAll + const core: NodeBadgeProps[] = [] + const extension: NodeBadgeProps[] = [] + const pricing: { required: string; rest?: string }[] = [] + if ( + settingStore.get('Comfy.NodeBadge.NodeLifeCycleBadgeMode') !== + NodeBadgeMode.None + ) { + const lifecycleText = nodeDef?.nodeLifeCycleBadgeText ?? '' + const trimmed = trim(lifecycleText, ['[', ']']) + if (trimmed) core.push({ text: trimmed }) + } + if ( + settingStore.get('Comfy.NodeBadge.NodeIdBadgeMode') !== NodeBadgeMode.None + ) + core.push({ text: `#${nodeData.id}` }) + const sourceText = nodeDef?.nodeSource?.badgeText + if ( + !nodeDef?.isCoreNode && + displaySource !== NodeBadgeMode.None && + sourceText + ) + core.push({ text: sourceText }) + + for (const badge of unpartitionedBadges.value.slice(1)) { + if (!badge.text) continue + + if (isCreditsBadge(badge)) { + const [required, rest] = splitAroundFirstSpace(badge.text) + pricing.push({ required, rest }) + continue + } + extension.push(badge) + } + + return { + hasComfyBadge: isCoreNode && pricing.length === 0, + core, + extension, + pricing + } + }) +} diff --git a/src/stores/workspace/rightSidePanelStore.ts b/src/stores/workspace/rightSidePanelStore.ts index ad4a26ae8fd..75e7b16bbfe 100644 --- a/src/stores/workspace/rightSidePanelStore.ts +++ b/src/stores/workspace/rightSidePanelStore.ts @@ -4,6 +4,7 @@ import { computed, ref, watch } from 'vue' import { useSettingStore } from '@/platform/settings/settingStore' export type RightSidePanelTab = + | 'error' | 'parameters' | 'nodes' | 'settings'