Skip to content

Commit 992c2ba

Browse files
Show text progress messages on executing nodes (#3824)
1 parent 4cc6a15 commit 992c2ba

File tree

7 files changed

+207
-2
lines changed

7 files changed

+207
-2
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<template>
2+
<div
3+
class="relative w-full text-xs min-h-[28px] max-h-[200px] rounded-lg px-4 py-2 overflow-y-auto"
4+
>
5+
<div class="flex items-center gap-2">
6+
<div class="flex-1 break-all flex items-center gap-2">
7+
<span v-html="formattedText"></span>
8+
<Skeleton v-if="isParentNodeExecuting" class="!flex-1 !h-4" />
9+
</div>
10+
</div>
11+
</div>
12+
</template>
13+
14+
<script setup lang="ts">
15+
import { NodeId } from '@comfyorg/litegraph'
16+
import Skeleton from 'primevue/skeleton'
17+
import { computed, onMounted, ref, watch } from 'vue'
18+
19+
import { useExecutionStore } from '@/stores/executionStore'
20+
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
21+
22+
const modelValue = defineModel<string>({ required: true })
23+
defineProps<{
24+
widget?: object
25+
}>()
26+
27+
const executionStore = useExecutionStore()
28+
const isParentNodeExecuting = ref(true)
29+
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
30+
31+
let executingNodeId: NodeId | null = null
32+
onMounted(() => {
33+
executingNodeId = executionStore.executingNodeId
34+
})
35+
36+
// Watch for either a new node has starting execution or overall execution ending
37+
const stopWatching = watch(
38+
[() => executionStore.executingNode, () => executionStore.isIdle],
39+
() => {
40+
if (
41+
executionStore.isIdle ||
42+
(executionStore.executingNode &&
43+
executionStore.executingNode.id !== executingNodeId)
44+
) {
45+
isParentNodeExecuting.value = false
46+
stopWatching()
47+
}
48+
if (!executingNodeId) {
49+
executingNodeId = executionStore.executingNodeId
50+
}
51+
}
52+
)
53+
</script>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { LGraphNode } from '@comfyorg/litegraph'
2+
3+
import { useTextPreviewWidget } from '@/composables/widgets/useProgressTextWidget'
4+
5+
const TEXT_PREVIEW_WIDGET_NAME = '$$node-text-preview'
6+
7+
/**
8+
* Composable for handling node text previews
9+
*/
10+
export function useNodeProgressText() {
11+
const textPreviewWidget = useTextPreviewWidget()
12+
13+
const findTextPreviewWidget = (node: LGraphNode) =>
14+
node.widgets?.find((w) => w.name === TEXT_PREVIEW_WIDGET_NAME)
15+
16+
const addTextPreviewWidget = (node: LGraphNode) =>
17+
textPreviewWidget(node, {
18+
name: TEXT_PREVIEW_WIDGET_NAME,
19+
type: 'progressText'
20+
})
21+
22+
/**
23+
* Shows text preview for a node
24+
* @param node The graph node to show the preview for
25+
*/
26+
function showTextPreview(node: LGraphNode, text: string) {
27+
const widget = findTextPreviewWidget(node) ?? addTextPreviewWidget(node)
28+
widget.value = text
29+
node.setDirtyCanvas?.(true)
30+
}
31+
32+
/**
33+
* Removes text preview from a node
34+
* @param node The graph node to remove the preview from
35+
*/
36+
function removeTextPreview(node: LGraphNode) {
37+
if (!node.widgets) return
38+
39+
const widgetIdx = node.widgets.findIndex(
40+
(w) => w.name === TEXT_PREVIEW_WIDGET_NAME
41+
)
42+
43+
if (widgetIdx > -1) {
44+
node.widgets[widgetIdx].onRemove?.()
45+
node.widgets.splice(widgetIdx, 1)
46+
}
47+
}
48+
49+
return {
50+
showTextPreview,
51+
removeTextPreview
52+
}
53+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { LGraphNode } from '@comfyorg/litegraph'
2+
import { ref } from 'vue'
3+
4+
import TextPreviewWidget from '@/components/graph/widgets/TextPreviewWidget.vue'
5+
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
6+
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
7+
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
8+
9+
const PADDING = 16
10+
11+
export const useTextPreviewWidget = () => {
12+
const widgetConstructor: ComfyWidgetConstructorV2 = (
13+
node: LGraphNode,
14+
inputSpec: InputSpec
15+
) => {
16+
const widgetValue = ref<string>('')
17+
const widget = new ComponentWidgetImpl<string | object>({
18+
node,
19+
name: inputSpec.name,
20+
component: TextPreviewWidget,
21+
inputSpec,
22+
options: {
23+
getValue: () => widgetValue.value,
24+
setValue: (value: string | object) => {
25+
widgetValue.value = typeof value === 'string' ? value : String(value)
26+
},
27+
getMinHeight: () => 42 + PADDING
28+
}
29+
})
30+
addWidget(node, widget)
31+
return widget
32+
}
33+
34+
return widgetConstructor
35+
}

src/schemas/apiSchema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ const zExecutionErrorWsMessage = zExecutionWsMessageBase.extend({
8282
current_outputs: z.any()
8383
})
8484

85+
const zProgressTextWsMessage = z.object({
86+
nodeId: zNodeId,
87+
text: z.string()
88+
})
89+
8590
const zTerminalSize = z.object({
8691
cols: z.number(),
8792
row: z.number()
@@ -114,6 +119,7 @@ export type ExecutionInterruptedWsMessage = z.infer<
114119
>
115120
export type ExecutionErrorWsMessage = z.infer<typeof zExecutionErrorWsMessage>
116121
export type LogsWsMessage = z.infer<typeof zLogsWsMessage>
122+
export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
117123
// End of ws messages
118124

119125
const zPromptInputItem = z.object({

src/scripts/api.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
LogsRawResponse,
1515
LogsWsMessage,
1616
PendingTaskItem,
17+
ProgressTextWsMessage,
1718
ProgressWsMessage,
1819
PromptResponse,
1920
RunningTaskItem,
@@ -101,6 +102,7 @@ interface BackendApiCalls {
101102
logs: LogsWsMessage
102103
/** Binary preview/progress data */
103104
b_preview: Blob
105+
progress_text: ProgressTextWsMessage
104106
}
105107

106108
/** Dictionary of all api calls */
@@ -399,12 +401,21 @@ export class ComfyApi extends EventTarget {
399401
if (event.data instanceof ArrayBuffer) {
400402
const view = new DataView(event.data)
401403
const eventType = view.getUint32(0)
402-
const imageType = view.getUint32(4)
403-
const imageData = event.data.slice(8)
404404

405405
let imageMime
406406
switch (eventType) {
407+
case 3:
408+
const decoder = new TextDecoder()
409+
const data = event.data.slice(4)
410+
const nodeIdLength = view.getUint32(4)
411+
this.dispatchCustomEvent('progress_text', {
412+
nodeId: decoder.decode(data.slice(4, 4 + nodeIdLength)),
413+
text: decoder.decode(data.slice(4 + nodeIdLength))
414+
})
415+
break
407416
case 1:
417+
const imageType = view.getUint32(4)
418+
const imageData = event.data.slice(8)
408419
switch (imageType) {
409420
case 2:
410421
imageMime = 'image/png'

src/stores/executionStore.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { defineStore } from 'pinia'
22
import { computed, ref } from 'vue'
33

4+
import { useNodeProgressText } from '@/composables/node/useNodeProgressText'
45
import type {
56
ExecutedWsMessage,
67
ExecutionCachedWsMessage,
78
ExecutionErrorWsMessage,
89
ExecutionStartWsMessage,
910
NodeError,
11+
ProgressTextWsMessage,
1012
ProgressWsMessage
1113
} from '@/schemas/apiSchema'
1214
import type {
@@ -103,6 +105,7 @@ export const useExecutionStore = defineStore('execution', () => {
103105
handleExecutionError as EventListener
104106
)
105107
}
108+
api.addEventListener('progress_text', handleProgressText as EventListener)
106109

107110
function unbindExecutionEvents() {
108111
api.removeEventListener(
@@ -121,6 +124,10 @@ export const useExecutionStore = defineStore('execution', () => {
121124
'execution_error',
122125
handleExecutionError as EventListener
123126
)
127+
api.removeEventListener(
128+
'progress_text',
129+
handleProgressText as EventListener
130+
)
124131
}
125132

126133
function handleExecutionStart(e: CustomEvent<ExecutionStartWsMessage>) {
@@ -177,6 +184,16 @@ export const useExecutionStore = defineStore('execution', () => {
177184
lastExecutionError.value = e.detail
178185
}
179186

187+
function handleProgressText(e: CustomEvent<ProgressTextWsMessage>) {
188+
const { nodeId, text } = e.detail
189+
if (!text || !nodeId) return
190+
191+
const node = app.graph.getNodeById(nodeId)
192+
if (!node) return
193+
194+
useNodeProgressText().showTextPreview(node, text)
195+
}
196+
180197
function storePrompt({
181198
nodes,
182199
id,

src/utils/formatUtil.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,33 @@ export function formatMetronomeCurrency(
472472
export function usdToMicros(usd: number): number {
473473
return Math.round(usd * 1_000_000)
474474
}
475+
476+
/**
477+
* Converts URLs in a string to HTML links.
478+
* @param text - The string to convert
479+
* @returns The string with URLs converted to HTML links
480+
* @example
481+
* 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'
482+
*/
483+
export function linkifyHtml(text: string): string {
484+
if (!text) return ''
485+
const urlRegex =
486+
/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%?=~_|])|(\bwww\.[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%?=~_|])/gi
487+
return text.replace(urlRegex, (_match, p1, _p2, p3) => {
488+
const url = p1 || p3
489+
const href = p3 ? `http://${url}` : url
490+
return `<a href="${href}" target="_blank" rel="noopener noreferrer" class="text-primary-400 hover:underline">${url}</a>`
491+
})
492+
}
493+
494+
/**
495+
* Converts newline characters to HTML <br> tags.
496+
* @param text - The string to convert
497+
* @returns The string with newline characters converted to <br> tags
498+
* @example
499+
* nl2br('Hello\nWorld') // returns 'Hello<br />World'
500+
*/
501+
export function nl2br(text: string): string {
502+
if (!text) return ''
503+
return text.replace(/\n/g, '<br />')
504+
}

0 commit comments

Comments
 (0)