mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
Feat: Add preview as plaintext toggle for Preview As Text (#7102)
## Summary Adds a toggle to conditionally render the text as Markdown. ## Also... Fixes some type issues across our myriad Widget types. We should probably clean those up. ## Example https://github.com/user-attachments/assets/24fed943-1e79-4ea4-a962-826b06d68761 This could be a good minimal testcase for dynamic widgets @AustinMroz ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7102-WIP-Feat-Add-preview-as-plaintext-toggle-for-Preview-As-Text-2bd6d73d3650810c8b25c84866c8875c) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -10,7 +10,10 @@ import type {
|
|||||||
INodeInputSlot,
|
INodeInputSlot,
|
||||||
INodeOutputSlot
|
INodeOutputSlot
|
||||||
} from '@/lib/litegraph/src/interfaces'
|
} from '@/lib/litegraph/src/interfaces'
|
||||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
import type {
|
||||||
|
IBaseWidget,
|
||||||
|
IWidgetOptions
|
||||||
|
} from '@/lib/litegraph/src/types/widgets'
|
||||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||||
import type { NodeId } from '@/renderer/core/layout/types'
|
import type { NodeId } from '@/renderer/core/layout/types'
|
||||||
@@ -39,7 +42,7 @@ export interface SafeWidgetData {
|
|||||||
type: string
|
type: string
|
||||||
value: WidgetValue
|
value: WidgetValue
|
||||||
label?: string
|
label?: string
|
||||||
options?: Record<string, unknown>
|
options?: IWidgetOptions<unknown>
|
||||||
callback?: ((value: unknown) => void) | undefined
|
callback?: ((value: unknown) => void) | undefined
|
||||||
spec?: InputSpec
|
spec?: InputSpec
|
||||||
slotMetadata?: WidgetSlotMetadata
|
slotMetadata?: WidgetSlotMetadata
|
||||||
@@ -107,7 +110,7 @@ export function safeWidgetMapper(
|
|||||||
type: widget.type,
|
type: widget.type,
|
||||||
value: value,
|
value: value,
|
||||||
label: widget.label,
|
label: widget.label,
|
||||||
options: widget.options ? { ...widget.options } : undefined,
|
options: widget.options,
|
||||||
callback: widget.callback,
|
callback: widget.callback,
|
||||||
spec,
|
spec,
|
||||||
slotMetadata: slotInfo,
|
slotMetadata: slotInfo,
|
||||||
|
|||||||
@@ -24,12 +24,43 @@ useExtensionService().registerExtension({
|
|||||||
app
|
app
|
||||||
).widget as DOMWidget<HTMLTextAreaElement, string>
|
).widget as DOMWidget<HTMLTextAreaElement, string>
|
||||||
|
|
||||||
showValueWidget.options.read_only = true
|
const showValueWidgetPlain = ComfyWidgets['STRING'](
|
||||||
|
this,
|
||||||
|
'preview',
|
||||||
|
['STRING', { multiline: true }],
|
||||||
|
app
|
||||||
|
).widget as DOMWidget<HTMLTextAreaElement, string>
|
||||||
|
|
||||||
|
const showAsPlaintextWidget = ComfyWidgets['BOOLEAN'](
|
||||||
|
this,
|
||||||
|
'previewMode',
|
||||||
|
[
|
||||||
|
'BOOLEAN',
|
||||||
|
{ label_on: 'Markdown', label_off: 'Plaintext', default: false }
|
||||||
|
],
|
||||||
|
app
|
||||||
|
)
|
||||||
|
|
||||||
|
showAsPlaintextWidget.widget.callback = (value) => {
|
||||||
|
showValueWidget.hidden = !value
|
||||||
|
showValueWidget.options.hidden = !value
|
||||||
|
showValueWidgetPlain.hidden = value
|
||||||
|
showValueWidgetPlain.options.hidden = value
|
||||||
|
}
|
||||||
|
|
||||||
|
showValueWidget.hidden = true
|
||||||
|
showValueWidget.options.hidden = true
|
||||||
|
showValueWidget.options.read_only = true
|
||||||
showValueWidget.element.readOnly = true
|
showValueWidget.element.readOnly = true
|
||||||
showValueWidget.element.disabled = true
|
showValueWidget.element.disabled = true
|
||||||
|
|
||||||
showValueWidget.serialize = false
|
showValueWidget.serialize = false
|
||||||
|
|
||||||
|
showValueWidgetPlain.hidden = false
|
||||||
|
showValueWidgetPlain.options.hidden = false
|
||||||
|
showValueWidgetPlain.options.read_only = true
|
||||||
|
showValueWidgetPlain.element.readOnly = true
|
||||||
|
showValueWidgetPlain.element.disabled = true
|
||||||
|
showValueWidgetPlain.serialize = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const onExecuted = nodeType.prototype.onExecuted
|
const onExecuted = nodeType.prototype.onExecuted
|
||||||
@@ -39,9 +70,10 @@ useExtensionService().registerExtension({
|
|||||||
? void 0
|
? void 0
|
||||||
: onExecuted.apply(this, [message])
|
: onExecuted.apply(this, [message])
|
||||||
|
|
||||||
const previewWidget = this.widgets?.find((w) => w.name === 'preview')
|
const previewWidgets =
|
||||||
|
this.widgets?.filter((w) => w.name === 'preview') ?? []
|
||||||
|
|
||||||
if (previewWidget) {
|
for (const previewWidget of previewWidgets) {
|
||||||
previewWidget.value = message.text[0]
|
previewWidget.value = message.text[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
|||||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { ComfyWidgets, addValueControlWidgets } from '@/scripts/widgets'
|
import {
|
||||||
|
ComfyWidgets,
|
||||||
|
addValueControlWidgets,
|
||||||
|
isValidWidgetType
|
||||||
|
} from '@/scripts/widgets'
|
||||||
import { CONFIG, GET_CONFIG } from '@/services/litegraphService'
|
import { CONFIG, GET_CONFIG } from '@/services/litegraphService'
|
||||||
import { mergeInputSpec } from '@/utils/nodeDefUtil'
|
import { mergeInputSpec } from '@/utils/nodeDefUtil'
|
||||||
import { applyTextReplacements } from '@/utils/searchAndReplace'
|
import { applyTextReplacements } from '@/utils/searchAndReplace'
|
||||||
@@ -223,8 +227,8 @@ export class PrimitiveNode extends LGraphNode {
|
|||||||
|
|
||||||
// Store current size as addWidget resizes the node
|
// Store current size as addWidget resizes the node
|
||||||
const [oldWidth, oldHeight] = this.size
|
const [oldWidth, oldHeight] = this.size
|
||||||
let widget: IBaseWidget | undefined
|
let widget: IBaseWidget
|
||||||
if (type in ComfyWidgets) {
|
if (isValidWidgetType(type)) {
|
||||||
widget = (ComfyWidgets[type](this, 'value', inputData, app) || {}).widget
|
widget = (ComfyWidgets[type](this, 'value', inputData, app) || {}).widget
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error InputSpec is not typed correctly
|
// @ts-expect-error InputSpec is not typed correctly
|
||||||
|
|||||||
@@ -4027,7 +4027,9 @@ export class LGraphNode
|
|||||||
w: IBaseWidget
|
w: IBaseWidget
|
||||||
}[] = []
|
}[] = []
|
||||||
|
|
||||||
for (const w of this.widgets) {
|
const visibleWidgets = this.widgets.filter((w) => !w.hidden)
|
||||||
|
|
||||||
|
for (const w of visibleWidgets) {
|
||||||
if (w.computeSize) {
|
if (w.computeSize) {
|
||||||
const height = w.computeSize()[1] + 4
|
const height = w.computeSize()[1] + 4
|
||||||
w.computedHeight = height
|
w.computedHeight = height
|
||||||
@@ -4066,7 +4068,7 @@ export class LGraphNode
|
|||||||
|
|
||||||
// Position widgets
|
// Position widgets
|
||||||
let y = startY
|
let y = startY
|
||||||
for (const w of this.widgets) {
|
for (const w of visibleWidgets) {
|
||||||
w.y = y
|
w.y = y
|
||||||
y += w.computedHeight ?? 0
|
y += w.computedHeight ?? 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ const nodeData = computed<VueNodeData>(() => {
|
|||||||
? input.options[0]
|
? input.options[0]
|
||||||
: '',
|
: '',
|
||||||
options: {
|
options: {
|
||||||
...input,
|
|
||||||
hidden: input.hidden,
|
hidden: input.hidden,
|
||||||
advanced: input.advanced,
|
advanced: input.advanced,
|
||||||
values: input.type === 'COMBO' ? input.options : undefined // For combo widgets
|
values: input.type === 'COMBO' ? input.options : undefined // For combo widgets
|
||||||
|
|||||||
@@ -16,46 +16,51 @@
|
|||||||
@pointermove="handleWidgetPointerEvent"
|
@pointermove="handleWidgetPointerEvent"
|
||||||
@pointerup="handleWidgetPointerEvent"
|
@pointerup="handleWidgetPointerEvent"
|
||||||
>
|
>
|
||||||
<div
|
<template
|
||||||
v-for="(widget, index) in processedWidgets"
|
v-for="(widget, index) in processedWidgets"
|
||||||
:key="`widget-${index}-${widget.name}`"
|
:key="`widget-${index}-${widget.name}`"
|
||||||
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch has-[.widget-expands]:flex-1"
|
|
||||||
>
|
>
|
||||||
<!-- Widget Input Slot Dot -->
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:class="
|
v-if="!widget.simplified.options?.hidden"
|
||||||
cn(
|
:data-is-hidden="`hidden: ${widget.simplified.options?.hidden}`"
|
||||||
'z-10 w-3 opacity-0 transition-opacity duration-150 group-hover:opacity-100 flex items-center',
|
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch has-[.widget-expands]:flex-1"
|
||||||
widget.slotMetadata?.linked && 'opacity-100'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<InputSlot
|
<!-- Widget Input Slot Dot -->
|
||||||
v-if="widget.slotMetadata"
|
|
||||||
:slot-data="{
|
<div
|
||||||
name: widget.name,
|
:class="
|
||||||
type: widget.type,
|
cn(
|
||||||
boundingRect: [0, 0, 0, 0]
|
'z-10 w-3 opacity-0 transition-opacity duration-150 group-hover:opacity-100 flex items-center',
|
||||||
}"
|
widget.slotMetadata?.linked && 'opacity-100'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<InputSlot
|
||||||
|
v-if="widget.slotMetadata"
|
||||||
|
:slot-data="{
|
||||||
|
name: widget.name,
|
||||||
|
type: widget.type,
|
||||||
|
boundingRect: [0, 0, 0, 0]
|
||||||
|
}"
|
||||||
|
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||||
|
:index="widget.slotMetadata.index"
|
||||||
|
:socketless="widget.simplified.spec?.socketless"
|
||||||
|
dot-only
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Widget Component -->
|
||||||
|
<component
|
||||||
|
:is="widget.vueComponent"
|
||||||
|
v-tooltip.left="widget.tooltipConfig"
|
||||||
|
:widget="widget.simplified"
|
||||||
|
:model-value="widget.value"
|
||||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||||
:index="widget.slotMetadata.index"
|
:node-type="nodeType"
|
||||||
:socketless="widget.simplified.spec?.socketless"
|
class="flex-1 col-span-2"
|
||||||
dot-only
|
@update:model-value="widget.updateHandler"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Widget Component -->
|
</template>
|
||||||
<component
|
|
||||||
:is="widget.vueComponent"
|
|
||||||
v-tooltip.left="widget.tooltipConfig"
|
|
||||||
:widget="widget.simplified"
|
|
||||||
:model-value="widget.value"
|
|
||||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
|
||||||
:node-type="nodeType"
|
|
||||||
class="flex-1 col-span-2"
|
|
||||||
@update:model-value="widget.updateHandler"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -132,25 +137,20 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
|||||||
const result: ProcessedWidget[] = []
|
const result: ProcessedWidget[] = []
|
||||||
|
|
||||||
for (const widget of widgets) {
|
for (const widget of widgets) {
|
||||||
// Skip if widget is in the hidden list for this node type
|
|
||||||
if (widget.options?.hidden) continue
|
|
||||||
if (widget.options?.canvasOnly) continue
|
|
||||||
if (!widget.type) continue
|
|
||||||
if (!shouldRenderAsVue(widget)) continue
|
if (!shouldRenderAsVue(widget)) continue
|
||||||
|
|
||||||
const vueComponent =
|
const vueComponent =
|
||||||
getComponent(widget.type, widget.name) ||
|
getComponent(widget.type, widget.name) ||
|
||||||
(widget.isDOMWidget ? WidgetDOM : WidgetLegacy)
|
(widget.isDOMWidget ? WidgetDOM : WidgetLegacy)
|
||||||
|
|
||||||
const slotMetadata = widget.slotMetadata
|
const { slotMetadata, options } = widget
|
||||||
|
|
||||||
let widgetOptions = widget.options
|
|
||||||
// Core feature: Disable Vue widgets when their input slots are connected
|
// Core feature: Disable Vue widgets when their input slots are connected
|
||||||
// This prevents conflicting input sources - when a slot is linked to another
|
// This prevents conflicting input sources - when a slot is linked to another
|
||||||
// node's output, the widget should be read-only to avoid data conflicts
|
// node's output, the widget should be read-only to avoid data conflicts
|
||||||
if (slotMetadata?.linked) {
|
const widgetOptions = slotMetadata?.linked
|
||||||
widgetOptions = { ...widget.options, disabled: true }
|
? { ...options, disabled: true }
|
||||||
}
|
: options
|
||||||
|
|
||||||
const simplified: SimplifiedWidget = {
|
const simplified: SimplifiedWidget = {
|
||||||
name: widget.name,
|
name: widget.name,
|
||||||
@@ -162,7 +162,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
|||||||
spec: widget.spec
|
spec: widget.spec
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateHandler = (value: WidgetValue) => {
|
function updateHandler(value: WidgetValue) {
|
||||||
// Update the widget value directly
|
// Update the widget value directly
|
||||||
widget.value = value
|
widget.value = value
|
||||||
|
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export function addValueControlWidgets(
|
|||||||
return widgets
|
return widgets
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
export const ComfyWidgets = {
|
||||||
INT: transformWidgetConstructorV2ToV1(useIntWidget()),
|
INT: transformWidgetConstructorV2ToV1(useIntWidget()),
|
||||||
FLOAT: transformWidgetConstructorV2ToV1(useFloatWidget()),
|
FLOAT: transformWidgetConstructorV2ToV1(useFloatWidget()),
|
||||||
BOOLEAN: transformWidgetConstructorV2ToV1(useBooleanWidget()),
|
BOOLEAN: transformWidgetConstructorV2ToV1(useBooleanWidget()),
|
||||||
@@ -299,4 +299,10 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
|||||||
GALLERIA: transformWidgetConstructorV2ToV1(useGalleriaWidget()),
|
GALLERIA: transformWidgetConstructorV2ToV1(useGalleriaWidget()),
|
||||||
TEXTAREA: transformWidgetConstructorV2ToV1(useTextareaWidget()),
|
TEXTAREA: transformWidgetConstructorV2ToV1(useTextareaWidget()),
|
||||||
...dynamicWidgets
|
...dynamicWidgets
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function isValidWidgetType(
|
||||||
|
key: unknown
|
||||||
|
): key is keyof typeof ComfyWidgets {
|
||||||
|
return ComfyWidgets[key as keyof typeof ComfyWidgets] !== undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
shouldRenderAsVue,
|
shouldRenderAsVue,
|
||||||
FOR_TESTING
|
FOR_TESTING
|
||||||
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||||
|
import type { SafeWidgetData } from '@/composables/graph/useGraphNodeManager'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
WidgetAudioUI,
|
WidgetAudioUI,
|
||||||
@@ -121,7 +122,10 @@ describe('widgetRegistry', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should respect options while checking type', () => {
|
it('should respect options while checking type', () => {
|
||||||
const widget = { type: 'text', options: { someOption: 'value' } }
|
const widget: Partial<SafeWidgetData> = {
|
||||||
|
type: 'text',
|
||||||
|
options: { precision: 5 }
|
||||||
|
}
|
||||||
expect(shouldRenderAsVue(widget)).toBe(true)
|
expect(shouldRenderAsVue(widget)).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user