diff --git a/.gitignore b/.gitignore index a7ed450..062af69 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,4 @@ experiments/ # Gemini model cache .gemini_models_cache.json +referance/ diff --git a/web/display_any.js b/web/display_any.js index 8a1da2f..b5c9b61 100644 --- a/web/display_any.js +++ b/web/display_any.js @@ -1,12 +1,67 @@ import { app } from "../../../scripts/app.js"; +import { ComfyWidgets } from "../../../scripts/widgets.js"; app.registerExtension({ name: "ComfyAssets.DisplayAny", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "DisplayAny") { + function populate(text) { + // Remove existing display widgets + if (this.widgets) { + const toRemove = []; + for (let i = 0; i < this.widgets.length; i++) { + if (this.widgets[i].name?.startsWith("display_")) { + this.widgets[i].onRemove?.(); + toRemove.push(i); + } + } + // Remove in reverse order to maintain indices + for (let i = toRemove.length - 1; i >= 0; i--) { + this.widgets.splice(toRemove[i], 1); + } + } + + // Split text into multiple widgets if it's too long or has multiple lines + const lines = text ? text.split('\n') : ['']; + const maxLinesPerWidget = 20; + const chunks = []; + + for (let i = 0; i < lines.length; i += maxLinesPerWidget) { + chunks.push(lines.slice(i, i + maxLinesPerWidget).join('\n')); + } + + // Create a widget for each chunk + chunks.forEach((chunk, index) => { + const w = ComfyWidgets["STRING"](this, `display_${index}`, ["STRING", { multiline: true }], app).widget; + w.inputEl.readOnly = true; + w.inputEl.style.opacity = 0.8; + w.inputEl.style.fontFamily = "monospace"; + w.value = chunk; + }); + + // Update node title with condensed info + const firstLine = text ? text.split('\n')[0] : ''; + const condensed = firstLine.length > 50 + ? firstLine.substring(0, 50) + "..." + : firstLine; + this.title = `DisplayAny: ${condensed}`; + + requestAnimationFrame(() => { + const sz = this.computeSize(); + if (sz[0] < this.size[0]) { + sz[0] = this.size[0]; + } + if (sz[1] < this.size[1]) { + sz[1] = this.size[1]; + } + this.onResize?.(sz); + app.graph.setDirtyCanvas(true, false); + }); + } + + // When the node is executed we will be sent the display text const onExecuted = nodeType.prototype.onExecuted; - - nodeType.prototype.onExecuted = function(message) { + nodeType.prototype.onExecuted = function (message) { onExecuted?.apply(this, arguments); console.log("DisplayAny onExecuted message:", message); @@ -14,181 +69,44 @@ app.registerExtension({ if (message?.text && message.text.length > 0) { const displayText = message.text[0]; console.log("DisplayAny displayText:", displayText); - - // Update the display widget with the value - this.updateDisplay(displayText); - - // Also show a condensed version in the title - const firstLine = displayText.split('\n')[0]; - const condensed = firstLine.length > 50 - ? firstLine.substring(0, 50) + "..." - : firstLine; - this.title = `DisplayAny: ${condensed}`; + populate.call(this, displayText); } else { console.log("DisplayAny no text in message"); } }; - - nodeType.prototype.updateDisplay = function(text) { - // Remove existing display widget if any - const existingWidget = this.widgets?.find(w => w.name === "display_value"); - if (existingWidget) { - const index = this.widgets.indexOf(existingWidget); - this.widgets.splice(index, 1); - } - - // Create display widget - const widget = { - type: "custom_display", - name: "display_value", - size: [this.size[0] - 20, 80], - displayText: text || "", - scrollY: 0, - maxScrollHeight: 0, - - draw: function(ctx, node, widget_width, y, H) { - const margin = 10; - const padding = 10; - const lineHeight = 16; - const minHeight = 60; - - // Fixed viewport height for scrollable area - const viewportHeight = 200; // Fixed height for display area - ctx.font = "12px monospace"; - const lines = this.displayText ? this.displayText.split('\n') : [""]; - - // Draw background - ctx.fillStyle = "#2a2a2a"; - ctx.fillRect(margin, y, widget_width - margin * 2, viewportHeight); - - // Draw border - ctx.strokeStyle = "#444"; - ctx.strokeRect(margin, y, widget_width - margin * 2, viewportHeight); - - // Draw text area background - ctx.fillStyle = "#1e1e1e"; - ctx.fillRect(margin + 1, y + 1, widget_width - margin * 2 - 2, viewportHeight - 2); - - // Save context for clipping - ctx.save(); - ctx.beginPath(); - ctx.rect(margin + 1, y + 1, widget_width - margin * 2 - 2, viewportHeight - 2); - ctx.clip(); - - // Prepare text - ctx.fillStyle = "#ddd"; - ctx.textAlign = "left"; - ctx.textBaseline = "top"; - - // Draw each line with scrolling - const maxWidth = widget_width - margin * 2 - padding * 2; - let currentY = y + padding - this.scrollY; - - // Process text - wrap long lines for JSON - let processedLines = []; - for (const line of lines) { - if (line.length > 0) { - // Split long lines into chunks that fit - let remaining = line; - while (remaining.length > 0) { - let chunkSize = remaining.length; - while (chunkSize > 0 && ctx.measureText(remaining.substring(0, chunkSize)).width > maxWidth) { - chunkSize--; - } - if (chunkSize === 0) chunkSize = 1; // At least one character - processedLines.push(remaining.substring(0, chunkSize)); - remaining = remaining.substring(chunkSize); - } - } else { - processedLines.push(line); - } - } - - // Calculate total content height for scrolling - const totalContentHeight = processedLines.length * lineHeight + padding * 2; - this.maxScrollHeight = Math.max(0, totalContentHeight - viewportHeight); - - // Draw all visible lines - for (let i = 0; i < processedLines.length; i++) { - // Only draw if line is in viewport - if (currentY > y - lineHeight && currentY < y + viewportHeight) { - ctx.fillText(processedLines[i], margin + padding, currentY); - } - currentY += lineHeight; - } - - // Restore context - ctx.restore(); - - // Draw scrollbar if needed - if (this.maxScrollHeight > 0) { - const scrollbarWidth = 6; - const scrollbarX = margin + widget_width - margin * 2 - scrollbarWidth - 2; - const scrollbarHeight = Math.max(20, (viewportHeight / totalContentHeight) * viewportHeight); - const scrollbarY = y + 2 + (this.scrollY / this.maxScrollHeight) * (viewportHeight - scrollbarHeight - 4); - - // Scrollbar track - ctx.fillStyle = "#333"; - ctx.fillRect(scrollbarX, y + 2, scrollbarWidth, viewportHeight - 4); - - // Scrollbar thumb - ctx.fillStyle = "#666"; - ctx.fillRect(scrollbarX, scrollbarY, scrollbarWidth, scrollbarHeight); - } - - return viewportHeight; - }, - - computeSize: function(width) { - // Fixed height for scrollable viewport - const viewportHeight = 200; - return [width, viewportHeight]; - } - }; - - // Add the widget - if (!this.widgets) { - this.widgets = []; - } - this.widgets.push(widget); - - // Adjust node size - this.computeSize(); - this.setDirtyCanvas(true); + + const VALUES = Symbol(); + const configure = nodeType.prototype.configure; + nodeType.prototype.configure = function () { + this[VALUES] = arguments[0]?.widgets_values; + return configure?.apply(this, arguments); }; - + + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + onConfigure?.apply(this, arguments); + const widgets_values = this[VALUES]; + if (widgets_values?.length) { + requestAnimationFrame(() => { + const startIdx = widgets_values.length > 1 && this.inputs?.[0]?.widget ? 1 : 0; + if (widgets_values[startIdx]) { + populate.call(this, widgets_values[startIdx]); + } + }); + } + }; + // Initialize on node creation const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { onNodeCreated?.apply(this, arguments); // Set minimum size - make it wider for better JSON display - this.size[0] = Math.max(this.size[0], 400); - this.size[1] = Math.max(this.size[1], 250); // Increased for viewport + this.size[0] = Math.max(this.size[0], 450); + this.size[1] = Math.max(this.size[1], 250); // Add placeholder text - this.updateDisplay("Value will appear here..."); - - // Mark this node as having a scrollable widget - this.flags = this.flags || {}; - this.flags.allow_interaction = true; - }; - - // Handle mouse wheel events on the node - const onMouseWheel = nodeType.prototype.onMouseWheel; - nodeType.prototype.onMouseWheel = function(event, local_pos, delta) { - // Check if we have a display widget - const displayWidget = this.widgets?.find(w => w.name === "display_value"); - if (displayWidget && displayWidget.maxScrollHeight > 0) { - // Scroll by 3 lines at a time - const scrollStep = 48; // 3 lines * 16px - displayWidget.scrollY = Math.max(0, Math.min(displayWidget.maxScrollHeight, displayWidget.scrollY - delta[1] * scrollStep)); - this.setDirtyCanvas(true); - return true; // Consume the event - } - - // Call original handler if exists - return onMouseWheel?.apply(this, arguments) || false; + populate.call(this, "Value will appear here..."); }; } } diff --git a/web/display_text.js b/web/display_text.js index 1c4e591..0fb3fad 100644 --- a/web/display_text.js +++ b/web/display_text.js @@ -5,608 +5,176 @@ app.registerExtension({ name: "ComfyAssets.DisplayText", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "DisplayText") { - const onExecuted = nodeType.prototype.onExecuted; - - nodeType.prototype.onExecuted = function(message) { - onExecuted?.apply(this, arguments); - - if (message?.text) { - // Create or update the text widget - this.updateTextDisplay(message.text[0]); + function populate(text) { + // Remove existing widgets + if (this.widgets) { + const toRemove = []; + for (let i = 0; i < this.widgets.length; i++) { + if (this.widgets[i].name?.startsWith("text_")) { + this.widgets[i].onRemove?.(); + toRemove.push(i); + } + } + // Remove in reverse order to maintain indices + for (let i = toRemove.length - 1; i >= 0; i--) { + this.widgets.splice(toRemove[i], 1); + } } - }; - - nodeType.prototype.updateTextDisplay = function(text) { - // Remove existing text widget if any - const existingWidget = this.widgets?.find(w => w.name === "displayed_text"); - if (existingWidget) { - const index = this.widgets.indexOf(existingWidget); - this.widgets.splice(index, 1); - } - + // Parse the text to detect positive/negative prompt format - function parsePrompts(text) { - const posMatch = text.match(/Positive prompt:\s*([\s\S]*?)(?=Negative prompt:|$)/i); - const negMatch = text.match(/Negative prompt:\s*([\s\S]*?)(?=\*\*|$)/i); + const posMatch = text.match(/Positive prompt:\s*([\s\S]*?)(?=Negative prompt:|$)/i); + const negMatch = text.match(/Negative prompt:\s*([\s\S]*?)(?=\*\*|$)/i); + + if (posMatch && negMatch) { + // Extract prompt content + let positiveText = posMatch[1].trim(); + let negativeText = negMatch[1].trim(); - if (posMatch && negMatch) { - // Extract just the prompt content, stopping at the first ** marker - let positiveText = posMatch[1].trim(); - let negativeText = negMatch[1].trim(); - - // Remove any trailing ** markers and everything after them - const posEndIndex = positiveText.indexOf('**'); - if (posEndIndex > 0) { - positiveText = positiveText.substring(0, posEndIndex).trim(); - } - - const negEndIndex = negativeText.indexOf('**'); - if (negEndIndex > 0) { - negativeText = negativeText.substring(0, negEndIndex).trim(); - } - - return { - type: 'prompts', - positive: positiveText, - negative: negativeText - }; + // Remove any trailing ** markers + const posEndIndex = positiveText.indexOf('**'); + if (posEndIndex > 0) { + positiveText = positiveText.substring(0, posEndIndex).trim(); } - return { - type: 'text', - content: text - }; - } - - const parsedContent = parsePrompts(text); - - // Create custom widget for text display - const widget = { - type: "custom_text_display", - name: "displayed_text", - size: [this.size[0] - 20, this.size[1] - 60], // Adjust for node chrome - parsedContent: parsedContent, - scrollOffset: 0, - posScrollOffset: 0, - negScrollOffset: 0, + const negEndIndex = negativeText.indexOf('**'); + if (negEndIndex > 0) { + negativeText = negativeText.substring(0, negEndIndex).trim(); + } - draw: function(ctx, node, widget_width, y, H) { - const margin = 10; - const padding = 10; - const lineHeight = 20; - const buttonHeight = 30; - const buttonWidth = 90; - - // Use the actual widget height from node size - const availableHeight = node.size[1] - 60; // Account for node header and margins - this.size[1] = Math.max(100, availableHeight); - - // Calculate available width for text - const availableWidth = widget_width - margin * 2 - padding * 2; - - // Word wrap function with better performance - function wrapText(text, maxWidth) { - const words = text.split(' '); - const lines = []; - let currentLine = ''; - - ctx.font = "14px monospace"; - - for (const word of words) { - const testLine = currentLine + (currentLine ? ' ' : '') + word; - const metrics = ctx.measureText(testLine); - - if (metrics.width > maxWidth && currentLine) { - lines.push(currentLine); - currentLine = word; - } else { - currentLine = testLine; - } - } - - if (currentLine) { - lines.push(currentLine); - } - - return lines.length > 0 ? lines : ['']; - } - - // Draw background - ctx.fillStyle = "#2a2a2a"; - ctx.fillRect(margin, y, widget_width - margin * 2, this.size[1]); - - // Draw border - ctx.strokeStyle = "#444"; - ctx.strokeRect(margin, y, widget_width - margin * 2, this.size[1]); - - if (this.parsedContent.type === 'prompts') { - // Simple split view for positive/negative prompts - const headerHeight = 25; - const buttonAreaHeight = buttonHeight + padding; - const totalTextHeight = this.size[1] - buttonAreaHeight; - const halfHeight = totalTextHeight / 2; - - // Draw positive prompt header - ctx.fillStyle = "#3a3a3a"; - ctx.fillRect(margin + 1, y + 1, widget_width - margin * 2 - 2, headerHeight); - ctx.fillStyle = "#8f8"; - ctx.font = "12px sans-serif"; - ctx.fillText("✓ Positive Prompt", margin + padding, y + headerHeight - 7); - - // Positive prompt text area - const posTextY = y + headerHeight; - const posTextHeight = halfHeight - headerHeight; - ctx.fillStyle = "#1e1e1e"; - ctx.fillRect(margin + 1, posTextY, widget_width - margin * 2 - 2, posTextHeight); - - // Draw separator - const separatorY = y + halfHeight; - ctx.strokeStyle = "#555"; - ctx.beginPath(); - ctx.moveTo(margin, separatorY); - ctx.lineTo(widget_width - margin, separatorY); - ctx.stroke(); - - // Draw negative prompt header - ctx.fillStyle = "#3a3a3a"; - ctx.fillRect(margin + 1, separatorY + 1, widget_width - margin * 2 - 2, headerHeight); - ctx.fillStyle = "#f88"; - ctx.font = "12px sans-serif"; - ctx.fillText("✗ Negative Prompt", margin + padding, separatorY + headerHeight - 7); - - // Negative prompt text area - const negTextY = separatorY + headerHeight; - const negTextHeight = halfHeight - headerHeight; - ctx.fillStyle = "#1e1e1e"; - ctx.fillRect(margin + 1, negTextY, widget_width - margin * 2 - 2, negTextHeight); - - // Draw text for both sections - ctx.font = "14px monospace"; - ctx.fillStyle = "#ddd"; - - // Wrap text for both prompts - const posLines = []; - const posParagraphs = this.parsedContent.positive.split('\n'); - for (const para of posParagraphs) { - if (para.trim() === '') { - posLines.push(''); - } else { - posLines.push(...wrapText(para, availableWidth - 10)); - } - } - - const negLines = []; - const negParagraphs = this.parsedContent.negative.split('\n'); - for (const para of negParagraphs) { - if (para.trim() === '') { - negLines.push(''); - } else { - negLines.push(...wrapText(para, availableWidth - 10)); - } - } - - // Draw positive prompt text with clipping - ctx.save(); - ctx.beginPath(); - ctx.rect(margin + padding, posTextY + padding, availableWidth - 10, posTextHeight - padding * 2); - ctx.clip(); - - let currentY = posTextY + padding + lineHeight - 5; - const posVisibleLines = Math.floor((posTextHeight - padding * 2) / lineHeight); - const posStartLine = Math.floor(this.posScrollOffset); - const posEndLine = Math.min(posStartLine + posVisibleLines, posLines.length); - - for (let i = posStartLine; i < posEndLine; i++) { - ctx.fillText(posLines[i], margin + padding, currentY); - currentY += lineHeight; - } - ctx.restore(); - - // Draw positive scroll indicator if needed - if (posLines.length > posVisibleLines) { - const scrollBarWidth = 6; - const scrollBarX = widget_width - margin - scrollBarWidth - 2; - const scrollBarHeight = posTextHeight - padding * 2; - const maxScroll = posLines.length - posVisibleLines; - const scrollRatio = this.posScrollOffset / maxScroll; - const thumbHeight = Math.max(20, (posVisibleLines / posLines.length) * scrollBarHeight); - const thumbY = posTextY + padding + scrollRatio * (scrollBarHeight - thumbHeight); - - ctx.fillStyle = "#333"; - ctx.fillRect(scrollBarX, posTextY + padding, scrollBarWidth, scrollBarHeight); - ctx.fillStyle = "#666"; - ctx.fillRect(scrollBarX, thumbY, scrollBarWidth, thumbHeight); - } - - // Draw negative prompt text with clipping - ctx.save(); - ctx.beginPath(); - ctx.rect(margin + padding, negTextY + padding, availableWidth - 10, negTextHeight - padding * 2); - ctx.clip(); - - ctx.fillStyle = "#ddd"; - currentY = negTextY + padding + lineHeight - 5; - const negVisibleLines = Math.floor((negTextHeight - padding * 2) / lineHeight); - const negStartLine = Math.floor(this.negScrollOffset); - const negEndLine = Math.min(negStartLine + negVisibleLines, negLines.length); - - for (let i = negStartLine; i < negEndLine; i++) { - ctx.fillText(negLines[i], margin + padding, currentY); - currentY += lineHeight; - } - ctx.restore(); - - // Draw negative scroll indicator if needed - if (negLines.length > negVisibleLines) { - const scrollBarWidth = 6; - const scrollBarX = widget_width - margin - scrollBarWidth - 2; - const scrollBarHeight = negTextHeight - padding * 2; - const maxScroll = negLines.length - negVisibleLines; - const scrollRatio = this.negScrollOffset / maxScroll; - const thumbHeight = Math.max(20, (negVisibleLines / negLines.length) * scrollBarHeight); - const thumbY = negTextY + padding + scrollRatio * (scrollBarHeight - thumbHeight); - - ctx.fillStyle = "#333"; - ctx.fillRect(scrollBarX, negTextY + padding, scrollBarWidth, scrollBarHeight); - ctx.fillStyle = "#666"; - ctx.fillRect(scrollBarX, thumbY, scrollBarWidth, thumbHeight); - } - - // Draw copy buttons - const buttonY = y + this.size[1] - buttonHeight - padding / 2; - const halfWidth = (widget_width - margin * 2) / 2; - - // Positive copy button - const posButtonX = margin + halfWidth / 2 - buttonWidth / 2; - ctx.fillStyle = this.posCopyHovered ? "#5a5a5a" : "#4a4a4a"; - ctx.fillRect(posButtonX, buttonY, buttonWidth, buttonHeight); - ctx.strokeStyle = "#666"; - ctx.strokeRect(posButtonX, buttonY, buttonWidth, buttonHeight); - - ctx.fillStyle = "#fff"; - ctx.font = "12px sans-serif"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.posCopySuccess ? "✓ Copied!" : "📋 Positive", posButtonX + buttonWidth/2, buttonY + buttonHeight/2); - - // Negative copy button - const negButtonX = margin + halfWidth + halfWidth / 2 - buttonWidth / 2; - ctx.fillStyle = this.negCopyHovered ? "#5a5a5a" : "#4a4a4a"; - ctx.fillRect(negButtonX, buttonY, buttonWidth, buttonHeight); - ctx.strokeStyle = "#666"; - ctx.strokeRect(negButtonX, buttonY, buttonWidth, buttonHeight); - - // Ensure text color and alignment are set - ctx.fillStyle = "#fff"; - ctx.font = "12px sans-serif"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.negCopySuccess ? "✓ Copied!" : "📋 Negative", negButtonX + buttonWidth/2, buttonY + buttonHeight/2); - - ctx.textAlign = "left"; - ctx.textBaseline = "alphabetic"; - - } else { - // Regular text display - const textAreaHeight = this.size[1] - buttonHeight - padding; - - // Draw text area background - ctx.fillStyle = "#1e1e1e"; - ctx.fillRect(margin + 1, y + 1, widget_width - margin * 2 - 2, textAreaHeight); - - // Process text - ctx.font = "14px monospace"; - const paragraphs = this.parsedContent.content.split('\n'); - const allLines = []; - - for (const paragraph of paragraphs) { - if (paragraph.trim() === '') { - allLines.push(''); - } else { - const wrappedLines = wrapText(paragraph, availableWidth); - allLines.push(...wrappedLines); - } - } - - // Draw text with clipping - ctx.save(); - ctx.beginPath(); - ctx.rect(margin + padding, y + padding, availableWidth, textAreaHeight - padding * 2); - ctx.clip(); - - const visibleLines = Math.floor((textAreaHeight - padding * 2) / lineHeight); - const maxScroll = Math.max(0, allLines.length - visibleLines); - this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll)); - - ctx.fillStyle = "#ddd"; - let currentY = y + padding + lineHeight - 5 - (this.scrollOffset * lineHeight); - - for (let i = 0; i < allLines.length; i++) { - if (currentY > y && currentY < y + textAreaHeight) { - ctx.fillText(allLines[i], margin + padding, currentY); - } - currentY += lineHeight; - } - - ctx.restore(); - - // Draw scroll indicator if needed - if (allLines.length > visibleLines) { - const scrollBarWidth = 6; - const scrollBarX = widget_width - margin - scrollBarWidth - 2; - const scrollBarHeight = textAreaHeight - 4; - const thumbHeight = Math.max(20, (visibleLines / allLines.length) * scrollBarHeight); - const thumbY = y + 2 + (this.scrollOffset / maxScroll) * (scrollBarHeight - thumbHeight); - - ctx.fillStyle = "#333"; - ctx.fillRect(scrollBarX, y + 2, scrollBarWidth, scrollBarHeight); - ctx.fillStyle = "#666"; - ctx.fillRect(scrollBarX, thumbY, scrollBarWidth, thumbHeight); - } - - // Draw copy button - const buttonX = widget_width - margin - buttonWidth - padding; - const buttonY = y + textAreaHeight + padding / 2; - - ctx.fillStyle = this.copyButtonHovered ? "#5a5a5a" : "#4a4a4a"; - ctx.fillRect(buttonX, buttonY, buttonWidth, buttonHeight); - ctx.strokeStyle = "#666"; - ctx.strokeRect(buttonX, buttonY, buttonWidth, buttonHeight); - - ctx.fillStyle = "#fff"; - ctx.font = "12px sans-serif"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.copySuccess ? "✓ Copied!" : "📋 Copy", buttonX + buttonWidth/2, buttonY + buttonHeight/2); - ctx.textAlign = "left"; - ctx.textBaseline = "alphabetic"; - } - - return this.size[1]; - }, + // Create header widget for positive prompt + const posHeader = ComfyWidgets["STRING"](this, "text_pos_header", ["STRING", { multiline: false }], app).widget; + posHeader.inputEl.readOnly = true; + posHeader.inputEl.style.opacity = 0.7; + posHeader.inputEl.style.backgroundColor = "#2a4a2a"; + posHeader.inputEl.style.color = "#8f8"; + posHeader.inputEl.style.fontWeight = "bold"; + posHeader.value = "✓ Positive Prompt"; - mouse: function(event, pos, node) { - const margin = 10; - const padding = 10; - const buttonWidth = 90; - const buttonHeight = 30; - const lineHeight = 20; - - // Check if mouse is over the widget - const isOver = pos[1] > this.last_y && pos[1] < this.last_y + this.size[1]; - if (!isOver) return false; - - if (this.parsedContent.type === 'prompts') { - // Handle split view - const headerHeight = 25; - const buttonAreaHeight = buttonHeight + padding; - const totalTextHeight = this.size[1] - buttonAreaHeight; - const halfHeight = totalTextHeight / 2; - - const posTextY = this.last_y + headerHeight; - const posTextHeight = halfHeight - headerHeight; - const negTextY = this.last_y + halfHeight + headerHeight; - const negTextHeight = halfHeight - headerHeight; - - const buttonY = this.last_y + this.size[1] - buttonHeight - padding / 2; - const halfWidth = (node.size[0] - margin * 2) / 2; - - // Check which section for scrolling - const inPosSection = pos[1] > posTextY && pos[1] < posTextY + posTextHeight; - const inNegSection = pos[1] > negTextY && pos[1] < negTextY + negTextHeight; - - // Handle scrolling - if (event.type === "wheel") { - if (inPosSection) { - const delta = event.deltaY > 0 ? 1 : -1; - this.posScrollOffset = (this.posScrollOffset || 0) + delta; - - // Calculate max scroll - const visibleLines = Math.floor((posTextHeight - padding * 2) / lineHeight); - const totalLines = this.parsedContent.positive.split('\n').length * 2; // Estimate - const maxScroll = Math.max(0, totalLines - visibleLines); - this.posScrollOffset = Math.max(0, Math.min(this.posScrollOffset, maxScroll)); - - node.setDirtyCanvas(true); - return true; - } else if (inNegSection) { - const delta = event.deltaY > 0 ? 1 : -1; - this.negScrollOffset = (this.negScrollOffset || 0) + delta; - - // Calculate max scroll - const visibleLines = Math.floor((negTextHeight - padding * 2) / lineHeight); - const totalLines = this.parsedContent.negative.split('\n').length * 2; // Estimate - const maxScroll = Math.max(0, totalLines - visibleLines); - this.negScrollOffset = Math.max(0, Math.min(this.negScrollOffset, maxScroll)); - - node.setDirtyCanvas(true); - return true; - } - } - - // Check button hovers - const posButtonX = margin + halfWidth / 2 - buttonWidth / 2; - const negButtonX = margin + halfWidth + halfWidth / 2 - buttonWidth / 2; - - const oldPosHover = this.posCopyHovered; - const oldNegHover = this.negCopyHovered; - - this.posCopyHovered = pos[0] > posButtonX && pos[0] < posButtonX + buttonWidth && - pos[1] > buttonY && pos[1] < buttonY + buttonHeight; - this.negCopyHovered = pos[0] > negButtonX && pos[0] < negButtonX + buttonWidth && - pos[1] > buttonY && pos[1] < buttonY + buttonHeight; - - if (oldPosHover !== this.posCopyHovered || oldNegHover !== this.negCopyHovered) { - node.setDirtyCanvas(true); - } - - // Handle button clicks - if (event.type === "pointerdown") { - if (this.posCopyHovered) { - this.copyToClipboard(this.parsedContent.positive, 'positive'); - return true; - } else if (this.negCopyHovered) { - this.copyToClipboard(this.parsedContent.negative, 'negative'); - return true; - } - } - } else { - // Regular text handling - const textAreaHeight = this.size[1] - buttonHeight - padding; - const buttonX = node.size[0] - margin - buttonWidth - padding; - const buttonY = this.last_y + textAreaHeight + padding / 2; - - // Handle scrolling - if (event.type === "wheel" && pos[1] < this.last_y + textAreaHeight) { - const delta = event.deltaY > 0 ? 1 : -1; - this.scrollOffset = (this.scrollOffset || 0) + delta; - - const visibleLines = Math.floor((textAreaHeight - padding * 2) / lineHeight); - const totalLines = this.parsedContent.content.split('\n').length * 2; // Estimate - const maxScroll = Math.max(0, totalLines - visibleLines); - this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll)); - - node.setDirtyCanvas(true); - return true; - } - - // Check button hover - const oldHover = this.copyButtonHovered; - this.copyButtonHovered = pos[0] > buttonX && pos[0] < buttonX + buttonWidth && - pos[1] > buttonY && pos[1] < buttonY + buttonHeight; - - if (oldHover !== this.copyButtonHovered) { - node.setDirtyCanvas(true); - } - - // Handle button click - if (event.type === "pointerdown" && this.copyButtonHovered) { - this.copyToClipboard(this.parsedContent.content, 'regular'); - return true; - } - } - - return false; - }, + // Create positive prompt widget + const posWidget = ComfyWidgets["STRING"](this, "text_positive", ["STRING", { multiline: true }], app).widget; + posWidget.inputEl.readOnly = true; + posWidget.inputEl.style.opacity = 0.9; + posWidget.value = positiveText; - copyToClipboard: function(text, type) { - const node = this._node; - navigator.clipboard.writeText(text).then(() => { - if (type === 'positive') { - this.posCopySuccess = true; - } else if (type === 'negative') { - this.negCopySuccess = true; - } else { - this.copySuccess = true; - } - node.setDirtyCanvas(true); - + // Add copy button for positive prompt + const posCopyBtn = document.createElement("button"); + posCopyBtn.textContent = "📋 Copy Positive"; + posCopyBtn.style.cssText = "margin: 5px; padding: 5px 10px; background: #4a4a4a; color: white; border: 1px solid #666; cursor: pointer; border-radius: 3px;"; + posCopyBtn.onclick = () => { + navigator.clipboard.writeText(positiveText).then(() => { + posCopyBtn.textContent = "✓ Copied!"; setTimeout(() => { - this.posCopySuccess = false; - this.negCopySuccess = false; - this.copySuccess = false; - node.setDirtyCanvas(true); + posCopyBtn.textContent = "📋 Copy Positive"; }, 1500); - }).catch(err => { - console.error('Failed to copy:', err); - // Fallback copy method - const textArea = document.createElement("textarea"); - textArea.value = text; - textArea.style.position = "fixed"; - textArea.style.opacity = "0"; - document.body.appendChild(textArea); - textArea.select(); - try { - document.execCommand('copy'); - if (type === 'positive') { - this.posCopySuccess = true; - } else if (type === 'negative') { - this.negCopySuccess = true; - } else { - this.copySuccess = true; - } - node.setDirtyCanvas(true); - setTimeout(() => { - this.posCopySuccess = false; - this.negCopySuccess = false; - this.copySuccess = false; - node.setDirtyCanvas(true); - }, 1500); - } catch (err) { - console.error('Fallback copy failed:', err); - } - document.body.removeChild(textArea); }); - }, + }; + posWidget.inputEl.parentNode.appendChild(posCopyBtn); - computeSize: function(width) { - return [width, this.size[1]]; + // Create header widget for negative prompt + const negHeader = ComfyWidgets["STRING"](this, "text_neg_header", ["STRING", { multiline: false }], app).widget; + negHeader.inputEl.readOnly = true; + negHeader.inputEl.style.opacity = 0.7; + negHeader.inputEl.style.backgroundColor = "#4a2a2a"; + negHeader.inputEl.style.color = "#f88"; + negHeader.inputEl.style.fontWeight = "bold"; + negHeader.value = "✗ Negative Prompt"; + + // Create negative prompt widget + const negWidget = ComfyWidgets["STRING"](this, "text_negative", ["STRING", { multiline: true }], app).widget; + negWidget.inputEl.readOnly = true; + negWidget.inputEl.style.opacity = 0.9; + negWidget.value = negativeText; + + // Add copy button for negative prompt + const negCopyBtn = document.createElement("button"); + negCopyBtn.textContent = "📋 Copy Negative"; + negCopyBtn.style.cssText = "margin: 5px; padding: 5px 10px; background: #4a4a4a; color: white; border: 1px solid #666; cursor: pointer; border-radius: 3px;"; + negCopyBtn.onclick = () => { + navigator.clipboard.writeText(negativeText).then(() => { + negCopyBtn.textContent = "✓ Copied!"; + setTimeout(() => { + negCopyBtn.textContent = "📋 Copy Negative"; + }, 1500); + }); + }; + negWidget.inputEl.parentNode.appendChild(negCopyBtn); + + } else { + // Single text display with ComfyUI's standard STRING widget + const w = ComfyWidgets["STRING"](this, "text_display", ["STRING", { multiline: true }], app).widget; + w.inputEl.readOnly = true; + w.inputEl.style.opacity = 0.9; + w.value = text; + + // Add copy button + const copyBtn = document.createElement("button"); + copyBtn.textContent = "📋 Copy"; + copyBtn.style.cssText = "margin: 5px; padding: 5px 10px; background: #4a4a4a; color: white; border: 1px solid #666; cursor: pointer; border-radius: 3px;"; + copyBtn.onclick = () => { + navigator.clipboard.writeText(text).then(() => { + copyBtn.textContent = "✓ Copied!"; + setTimeout(() => { + copyBtn.textContent = "📋 Copy"; + }, 1500); + }); + }; + w.inputEl.parentNode.appendChild(copyBtn); + } + + requestAnimationFrame(() => { + const sz = this.computeSize(); + if (sz[0] < this.size[0]) { + sz[0] = this.size[0]; } - }; - - // Store reference to node for callbacks - widget._node = this; - - // Store the last y position for mouse detection - const originalDraw = widget.draw; - widget.draw = function(ctx, node, widget_width, y, H) { - this.last_y = y; - return originalDraw.call(this, ctx, node, widget_width, y, H); - }; - - // Add the widget - if (!this.widgets) { - this.widgets = []; - } - this.widgets.push(widget); - - // Adjust node size to accommodate the widget - this.computeSize(); - }; - - // Handle node resizing - const onResize = nodeType.prototype.onResize; - nodeType.prototype.onResize = function(size) { - onResize?.apply(this, arguments); - - // Update widget size when node is resized - const textWidget = this.widgets?.find(w => w.name === "displayed_text"); - if (textWidget) { - textWidget.size[0] = size[0] - 20; - textWidget.size[1] = size[1] - 60; - this.setDirtyCanvas(true); + if (sz[1] < this.size[1]) { + sz[1] = this.size[1]; + } + this.onResize?.(sz); + app.graph.setDirtyCanvas(true, false); + }); + } + + // When the node is executed we will be sent the input text + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + if (message?.text) { + populate.call(this, message.text[0]); } }; - + + const VALUES = Symbol(); + const configure = nodeType.prototype.configure; + nodeType.prototype.configure = function () { + this[VALUES] = arguments[0]?.widgets_values; + return configure?.apply(this, arguments); + }; + + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + onConfigure?.apply(this, arguments); + const widgets_values = this[VALUES]; + if (widgets_values?.length) { + requestAnimationFrame(() => { + const startIdx = widgets_values.length > 1 && this.inputs?.[0]?.widget ? 1 : 0; + if (widgets_values[startIdx]) { + populate.call(this, widgets_values[startIdx]); + } + }); + } + }; + // Initialize on node creation const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { onNodeCreated?.apply(this, arguments); // Set default size - this.size[0] = Math.max(this.size[0], 350); + this.size[0] = Math.max(this.size[0], 400); this.size[1] = Math.max(this.size[1], 300); // Add placeholder text - this.updateTextDisplay("Text will appear here after execution..."); - - // Mark this node as having a scrollable widget - this.flags = this.flags || {}; - this.flags.allow_interaction = true; - }; - - // Override mouse wheel handler at node level - const onMouseWheel = nodeType.prototype.onMouseWheel; - nodeType.prototype.onMouseWheel = function(event, local_pos, delta) { - const textWidget = this.widgets?.find(w => w.name === "displayed_text"); - if (textWidget) { - // Let the widget handle the mouse event - const fakeEvent = { type: "wheel", deltaY: -delta[1] * 100 }; - if (textWidget.mouse && textWidget.mouse.call(textWidget, fakeEvent, local_pos, this)) { - return true; - } - } - return onMouseWheel?.apply(this, arguments) || false; + populate.call(this, "Text will appear here after execution..."); }; } }