mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary This PR adds a minimal, backward-compatible way to render **labeled hyperlinks** in node text previews without enabling full Markdown rendering. The syntax is: ``` [[Label|https://example.com]] ``` Links open in a new tab and preserve the existing look and behavior of the widget. ## Motivation I first implemented a `Markdown`-based version that correctly rendered `[label](url)` and other inline Markdown. After some consideration, I have decided against shipping Markdown here because it risks breaking existing custom nodes that already rely on `PromptServer.instance.send_progress_text` with the current `linkifyHtml` --> `nl2br` behavior. The token approach is **smaller**, safer, and avoids surprises. ## Changes * No global behavior change. * No new dependencies. * Token parsing is opt-in. If a node does not emit `[[label|url]]`, behavior is unchanged. ## ComfyUI backend changes PR to ComfyUI with these will come later(as first version of ComfyUI with these frontend changes should be released), and it will contain: ```python def _display_text( node_cls: type[IO.ComfyNode], text: Optional[str], *, status: Optional[Union[str, int]] = None, price: Optional[float] = None, results: Optional[Union[list[str], str]] = None, ) -> None: display_lines: list[str] = [] if status: display_lines.append(f"Status: {status.capitalize() if isinstance(status, str) else status}") if price is not None: display_lines.append(f"Price: ${float(price):,.4f}") if results: # New code starts if isinstance(results, str): display_lines.append(f"Result link: [[1|{results}]]") elif len(results) == 1: display_lines.append(f"Result link: [[1|{results[0]}]]") else: links = ", ".join(f"[[{i}|{u}]]" for i, u in enumerate(results, start=1)) display_lines.append(f"Result links: {links}") # New code ends if text is not None: display_lines.append(text) if display_lines: PromptServer.instance.send_progress_text("\n".join(display_lines), get_node_id(node_cls)) ``` ## Screenshots (if applicable) <img width="692" height="716" alt="Screenshot From 2025-10-31 13-12-54" src="https://github.com/user-attachments/assets/619b5f70-550c-442f-9cd9-05a95270e533" /> <img width="732" height="714" alt="Screenshot From 2025-10-31 13-14-15" src="https://github.com/user-attachments/assets/836ff87b-a2ac-45ba-842c-b0a4af91c7de" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6482-feat-TextPreviewWidget-add-minimal-support-for-label-url-links-29d6d73d365081e9ac97dd7f41e85d8f) by [Unito](https://www.unito.io)
91 lines
2.7 KiB
Vue
91 lines
2.7 KiB
Vue
<template>
|
|
<div
|
|
class="relative max-h-[200px] min-h-[28px] w-full overflow-y-auto rounded-lg px-4 py-2 text-xs"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<div class="flex flex-1 items-center gap-2 break-all">
|
|
<span v-html="formattedText"></span>
|
|
<Skeleton v-if="isParentNodeExecuting" class="h-4! flex-1!" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import Skeleton from 'primevue/skeleton'
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
|
|
import type { NodeId } from '@/lib/litegraph/src/litegraph'
|
|
import { useExecutionStore } from '@/stores/executionStore'
|
|
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
|
|
|
const modelValue = defineModel<string>({ required: true })
|
|
const props = defineProps<{
|
|
widget?: object
|
|
nodeId: NodeId
|
|
}>()
|
|
|
|
const executionStore = useExecutionStore()
|
|
const isParentNodeExecuting = ref(true)
|
|
const formattedText = computed(() => {
|
|
const src = modelValue.value
|
|
// Turn [[label|url]] into placeholders to avoid interfering with linkifyHtml
|
|
const tokens: { label: string; url: string }[] = []
|
|
const holed = src.replace(
|
|
/\[\[([^|\]]+)\|([^\]]+)\]\]/g,
|
|
(_m, label, url) => {
|
|
tokens.push({ label: String(label), url: String(url) })
|
|
return `__LNK${tokens.length - 1}__`
|
|
}
|
|
)
|
|
|
|
// Keep current behavior (auto-link bare URLs + \n -> <br>)
|
|
let html = nl2br(linkifyHtml(holed))
|
|
|
|
// Restore placeholders as <a>...</a> (minimal escaping + http default)
|
|
html = html.replace(/__LNK(\d+)__/g, (_m, i) => {
|
|
const { label, url } = tokens[+i]
|
|
const safeHref = url.replace(/"/g, '"')
|
|
const safeLabel = label.replace(/</g, '<').replace(/>/g, '>')
|
|
return /^https?:\/\//i.test(url)
|
|
? `<a href="${safeHref}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`
|
|
: safeLabel
|
|
})
|
|
|
|
return html
|
|
})
|
|
|
|
let parentNodeId: NodeId | null = null
|
|
onMounted(() => {
|
|
// Get the parent node ID from props if provided
|
|
// For backward compatibility, fall back to the first executing node
|
|
parentNodeId = props.nodeId
|
|
})
|
|
|
|
// Watch for either a new node has starting execution or overall execution ending
|
|
const stopWatching = watch(
|
|
[() => executionStore.executingNodeIds, () => executionStore.isIdle],
|
|
() => {
|
|
if (executionStore.isIdle) {
|
|
isParentNodeExecuting.value = false
|
|
stopWatching()
|
|
return
|
|
}
|
|
|
|
// Check if parent node is no longer in the executing nodes list
|
|
if (
|
|
parentNodeId &&
|
|
!executionStore.executingNodeIds.includes(parentNodeId)
|
|
) {
|
|
isParentNodeExecuting.value = false
|
|
stopWatching()
|
|
}
|
|
|
|
// Set parent node ID if not set yet
|
|
if (!parentNodeId && executionStore.executingNodeIds.length > 0) {
|
|
parentNodeId = executionStore.executingNodeIds[0]
|
|
}
|
|
}
|
|
)
|
|
</script>
|