diff --git a/src/components/graph/widgets/TextPreviewWidget.vue b/src/components/graph/widgets/TextPreviewWidget.vue
new file mode 100644
index 000000000..8d161b8e3
--- /dev/null
+++ b/src/components/graph/widgets/TextPreviewWidget.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
diff --git a/src/composables/node/useNodeProgressText.ts b/src/composables/node/useNodeProgressText.ts
new file mode 100644
index 000000000..764b3413f
--- /dev/null
+++ b/src/composables/node/useNodeProgressText.ts
@@ -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
+ }
+}
diff --git a/src/composables/widgets/useProgressTextWidget.ts b/src/composables/widgets/useProgressTextWidget.ts
new file mode 100644
index 000000000..f6c56efbd
--- /dev/null
+++ b/src/composables/widgets/useProgressTextWidget.ts
@@ -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('')
+ const widget = new ComponentWidgetImpl({
+ 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
+}
diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts
index f94f91c3d..88990b311 100644
--- a/src/schemas/apiSchema.ts
+++ b/src/schemas/apiSchema.ts
@@ -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
export type LogsWsMessage = z.infer
+export type ProgressTextWsMessage = z.infer
// End of ws messages
const zPromptInputItem = z.object({
diff --git a/src/scripts/api.ts b/src/scripts/api.ts
index ba6cb569e..a40e8936d 100644
--- a/src/scripts/api.ts
+++ b/src/scripts/api.ts
@@ -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'
diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts
index 554ae5a88..8ce1a35a0 100644
--- a/src/stores/executionStore.ts
+++ b/src/stores/executionStore.ts
@@ -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) {
@@ -177,6 +184,16 @@ export const useExecutionStore = defineStore('execution', () => {
lastExecutionError.value = e.detail
}
+ function handleProgressText(e: CustomEvent) {
+ 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,
diff --git a/src/utils/formatUtil.ts b/src/utils/formatUtil.ts
index e4d04baaa..e80814dc9 100644
--- a/src/utils/formatUtil.ts
+++ b/src/utils/formatUtil.ts
@@ -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 https://example.com 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 `${url}`
+ })
+}
+
+/**
+ * Converts newline characters to HTML
tags.
+ * @param text - The string to convert
+ * @returns The string with newline characters converted to
tags
+ * @example
+ * nl2br('Hello\nWorld') // returns 'Hello
World'
+ */
+export function nl2br(text: string): string {
+ if (!text) return ''
+ return text.replace(/\n/g, '
')
+}