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, '
') +}