mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Show text progress messages on executing nodes (#3824)
This commit is contained in:
53
src/components/graph/widgets/TextPreviewWidget.vue
Normal file
53
src/components/graph/widgets/TextPreviewWidget.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative w-full text-xs min-h-[28px] max-h-[200px] rounded-lg px-4 py-2 overflow-y-auto"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 break-all flex items-center gap-2">
|
||||
<span v-html="formattedText"></span>
|
||||
<Skeleton v-if="isParentNodeExecuting" class="!flex-1 !h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NodeId } from '@comfyorg/litegraph'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
defineProps<{
|
||||
widget?: object
|
||||
}>()
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const isParentNodeExecuting = ref(true)
|
||||
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
||||
|
||||
let executingNodeId: NodeId | null = null
|
||||
onMounted(() => {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
})
|
||||
|
||||
// Watch for either a new node has starting execution or overall execution ending
|
||||
const stopWatching = watch(
|
||||
[() => executionStore.executingNode, () => executionStore.isIdle],
|
||||
() => {
|
||||
if (
|
||||
executionStore.isIdle ||
|
||||
(executionStore.executingNode &&
|
||||
executionStore.executingNode.id !== executingNodeId)
|
||||
) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
}
|
||||
if (!executingNodeId) {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
53
src/composables/node/useNodeProgressText.ts
Normal file
53
src/composables/node/useNodeProgressText.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useTextPreviewWidget } from '@/composables/widgets/useProgressTextWidget'
|
||||
|
||||
const TEXT_PREVIEW_WIDGET_NAME = '$$node-text-preview'
|
||||
|
||||
/**
|
||||
* Composable for handling node text previews
|
||||
*/
|
||||
export function useNodeProgressText() {
|
||||
const textPreviewWidget = useTextPreviewWidget()
|
||||
|
||||
const findTextPreviewWidget = (node: LGraphNode) =>
|
||||
node.widgets?.find((w) => w.name === TEXT_PREVIEW_WIDGET_NAME)
|
||||
|
||||
const addTextPreviewWidget = (node: LGraphNode) =>
|
||||
textPreviewWidget(node, {
|
||||
name: TEXT_PREVIEW_WIDGET_NAME,
|
||||
type: 'progressText'
|
||||
})
|
||||
|
||||
/**
|
||||
* Shows text preview for a node
|
||||
* @param node The graph node to show the preview for
|
||||
*/
|
||||
function showTextPreview(node: LGraphNode, text: string) {
|
||||
const widget = findTextPreviewWidget(node) ?? addTextPreviewWidget(node)
|
||||
widget.value = text
|
||||
node.setDirtyCanvas?.(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes text preview from a node
|
||||
* @param node The graph node to remove the preview from
|
||||
*/
|
||||
function removeTextPreview(node: LGraphNode) {
|
||||
if (!node.widgets) return
|
||||
|
||||
const widgetIdx = node.widgets.findIndex(
|
||||
(w) => w.name === TEXT_PREVIEW_WIDGET_NAME
|
||||
)
|
||||
|
||||
if (widgetIdx > -1) {
|
||||
node.widgets[widgetIdx].onRemove?.()
|
||||
node.widgets.splice(widgetIdx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showTextPreview,
|
||||
removeTextPreview
|
||||
}
|
||||
}
|
||||
35
src/composables/widgets/useProgressTextWidget.ts
Normal file
35
src/composables/widgets/useProgressTextWidget.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import TextPreviewWidget from '@/components/graph/widgets/TextPreviewWidget.vue'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
const PADDING = 16
|
||||
|
||||
export const useTextPreviewWidget = () => {
|
||||
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpec
|
||||
) => {
|
||||
const widgetValue = ref<string>('')
|
||||
const widget = new ComponentWidgetImpl<string | object>({
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: TextPreviewWidget,
|
||||
inputSpec,
|
||||
options: {
|
||||
getValue: () => widgetValue.value,
|
||||
setValue: (value: string | object) => {
|
||||
widgetValue.value = typeof value === 'string' ? value : String(value)
|
||||
},
|
||||
getMinHeight: () => 42 + PADDING
|
||||
}
|
||||
})
|
||||
addWidget(node, widget)
|
||||
return widget
|
||||
}
|
||||
|
||||
return widgetConstructor
|
||||
}
|
||||
@@ -82,6 +82,11 @@ const zExecutionErrorWsMessage = zExecutionWsMessageBase.extend({
|
||||
current_outputs: z.any()
|
||||
})
|
||||
|
||||
const zProgressTextWsMessage = z.object({
|
||||
nodeId: zNodeId,
|
||||
text: z.string()
|
||||
})
|
||||
|
||||
const zTerminalSize = z.object({
|
||||
cols: z.number(),
|
||||
row: z.number()
|
||||
@@ -114,6 +119,7 @@ export type ExecutionInterruptedWsMessage = z.infer<
|
||||
>
|
||||
export type ExecutionErrorWsMessage = z.infer<typeof zExecutionErrorWsMessage>
|
||||
export type LogsWsMessage = z.infer<typeof zLogsWsMessage>
|
||||
export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
|
||||
// End of ws messages
|
||||
|
||||
const zPromptInputItem = z.object({
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
LogsRawResponse,
|
||||
LogsWsMessage,
|
||||
PendingTaskItem,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage,
|
||||
PromptResponse,
|
||||
RunningTaskItem,
|
||||
@@ -101,6 +102,7 @@ interface BackendApiCalls {
|
||||
logs: LogsWsMessage
|
||||
/** Binary preview/progress data */
|
||||
b_preview: Blob
|
||||
progress_text: ProgressTextWsMessage
|
||||
}
|
||||
|
||||
/** Dictionary of all api calls */
|
||||
@@ -399,12 +401,21 @@ export class ComfyApi extends EventTarget {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
const view = new DataView(event.data)
|
||||
const eventType = view.getUint32(0)
|
||||
const imageType = view.getUint32(4)
|
||||
const imageData = event.data.slice(8)
|
||||
|
||||
let imageMime
|
||||
switch (eventType) {
|
||||
case 3:
|
||||
const decoder = new TextDecoder()
|
||||
const data = event.data.slice(4)
|
||||
const nodeIdLength = view.getUint32(4)
|
||||
this.dispatchCustomEvent('progress_text', {
|
||||
nodeId: decoder.decode(data.slice(4, 4 + nodeIdLength)),
|
||||
text: decoder.decode(data.slice(4 + nodeIdLength))
|
||||
})
|
||||
break
|
||||
case 1:
|
||||
const imageType = view.getUint32(4)
|
||||
const imageData = event.data.slice(8)
|
||||
switch (imageType) {
|
||||
case 2:
|
||||
imageMime = 'image/png'
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useNodeProgressText } from '@/composables/node/useNodeProgressText'
|
||||
import type {
|
||||
ExecutedWsMessage,
|
||||
ExecutionCachedWsMessage,
|
||||
ExecutionErrorWsMessage,
|
||||
ExecutionStartWsMessage,
|
||||
NodeError,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage
|
||||
} from '@/schemas/apiSchema'
|
||||
import type {
|
||||
@@ -103,6 +105,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
handleExecutionError as EventListener
|
||||
)
|
||||
}
|
||||
api.addEventListener('progress_text', handleProgressText as EventListener)
|
||||
|
||||
function unbindExecutionEvents() {
|
||||
api.removeEventListener(
|
||||
@@ -121,6 +124,10 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
'execution_error',
|
||||
handleExecutionError as EventListener
|
||||
)
|
||||
api.removeEventListener(
|
||||
'progress_text',
|
||||
handleProgressText as EventListener
|
||||
)
|
||||
}
|
||||
|
||||
function handleExecutionStart(e: CustomEvent<ExecutionStartWsMessage>) {
|
||||
@@ -177,6 +184,16 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
lastExecutionError.value = e.detail
|
||||
}
|
||||
|
||||
function handleProgressText(e: CustomEvent<ProgressTextWsMessage>) {
|
||||
const { nodeId, text } = e.detail
|
||||
if (!text || !nodeId) return
|
||||
|
||||
const node = app.graph.getNodeById(nodeId)
|
||||
if (!node) return
|
||||
|
||||
useNodeProgressText().showTextPreview(node, text)
|
||||
}
|
||||
|
||||
function storePrompt({
|
||||
nodes,
|
||||
id,
|
||||
|
||||
@@ -472,3 +472,33 @@ export function formatMetronomeCurrency(
|
||||
export function usdToMicros(usd: number): number {
|
||||
return Math.round(usd * 1_000_000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts URLs in a string to HTML links.
|
||||
* @param text - The string to convert
|
||||
* @returns The string with URLs converted to HTML links
|
||||
* @example
|
||||
* linkifyHtml('Visit https://example.com for more info') // returns 'Visit <a href="https://example.com" target="_blank" rel="noopener noreferrer" class="text-primary-400 hover:underline">https://example.com</a> for more info'
|
||||
*/
|
||||
export function linkifyHtml(text: string): string {
|
||||
if (!text) return ''
|
||||
const urlRegex =
|
||||
/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%?=~_|])|(\bwww\.[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%?=~_|])/gi
|
||||
return text.replace(urlRegex, (_match, p1, _p2, p3) => {
|
||||
const url = p1 || p3
|
||||
const href = p3 ? `http://${url}` : url
|
||||
return `<a href="${href}" target="_blank" rel="noopener noreferrer" class="text-primary-400 hover:underline">${url}</a>`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts newline characters to HTML <br> tags.
|
||||
* @param text - The string to convert
|
||||
* @returns The string with newline characters converted to <br> tags
|
||||
* @example
|
||||
* nl2br('Hello\nWorld') // returns 'Hello<br />World'
|
||||
*/
|
||||
export function nl2br(text: string): string {
|
||||
if (!text) return ''
|
||||
return text.replace(/\n/g, '<br />')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user