import { app } from "../../../scripts/app.js"; import { ComfyWidgets } from "../../../scripts/widgets.js"; import { defaultPresets, defaultTemplates, defaultThemeConfig, injectDialogCSS, DialogUtils } from "./MultiTextDefaults.js"; const THEME = { ...defaultThemeConfig, promptPresets: defaultPresets, promptTemplates: defaultTemplates, }; injectDialogCSS(THEME); const fontStyles = document.createElement('style'); fontStyles.textContent = ` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'); .multitext-node { font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } `; document.head.appendChild(fontStyles); const Utils = { drawRoundedRect(ctx, x, y, w, h, r = 4, options = { fill: true, stroke: false, lineWidth: 1 }) { ctx.beginPath(); ctx.roundRect(x, y, w, h, r); if (options.fill) ctx.fill(); if (options.stroke) { ctx.lineWidth = options.lineWidth; ctx.stroke(); } }, truncateTextEfficient(ctx, text, maxWidth) { if (!text) return ''; const width = ctx.measureText(text).width; if (width <= maxWidth) return text; const ellipsis = '...'; const ellipsisWidth = ctx.measureText(ellipsis).width; let low = 0; let high = text.length; let best = 0; while (low <= high) { const mid = (low + high) >>> 1; const slice = text.slice(0, mid); const width = ctx.measureText(slice).width + ellipsisWidth; if (width <= maxWidth) { best = mid; low = mid + 1; } else { high = mid - 1; } } return text.slice(0, best) + ellipsis; } }; const UXUtils = { drawTooltip(ctx, text, x, y) { if (!text) return; const padding = 8; const fontSize = 12; ctx.save(); ctx.font = `${fontSize}px ${THEME.typography.fonts.primary}`; ctx.textBaseline = "middle"; ctx.textAlign = "left"; const metrics = ctx.measureText(text); const tooltipWidth = metrics.width + (padding * 2); const tooltipHeight = fontSize + (padding * 2); let tooltipX = Math.max(8, Math.min(x, ctx.canvas.width - tooltipWidth - 8)); let tooltipY = y - tooltipHeight - 8; if (tooltipY < 8) { tooltipY = y + 24; } ctx.fillStyle = THEME.colors.bgActive; ctx.shadowColor = "rgba(0,0,0,0.2)"; ctx.shadowBlur = 8; ctx.shadowOffsetY = 2; Utils.drawRoundedRect(ctx, tooltipX, tooltipY, tooltipWidth, tooltipHeight, 4); ctx.shadowColor = "transparent"; ctx.fillStyle = THEME.colors.text; ctx.fillText(text, tooltipX + padding, tooltipY + (tooltipHeight / 2)); ctx.restore(); } }; const Renderer = { drawPromptRow(ctx, x, y, text, weight, rowWidth, rowHeight, radius) { ctx.save(); ctx.fillStyle = THEME.colors.bg; ctx.shadowColor = "rgba(0,0,0,0.06)"; ctx.shadowBlur = 2; ctx.shadowOffsetY = 1; Utils.drawRoundedRect(ctx, x, y, rowWidth, rowHeight, radius); ctx.restore(); ctx.strokeStyle = THEME.colors.border; Utils.drawRoundedRect(ctx, x, y, rowWidth, rowHeight, radius, { fill: false, stroke: true, lineWidth: THEME.layout.promptRow.strokeWidth }); ctx.fillStyle = THEME.colors.text; ctx.font = `400 11px ${THEME.typography.fonts.primary}`; const truncatedText = Utils.truncateTextEfficient(ctx, text, rowWidth - 100); const toggleWidth = 15; ctx.fillText(truncatedText, x + toggleWidth + 2, y + rowHeight/2 + 4); const weightX = x + rowWidth - 55; const weightY = Math.floor(y + (rowHeight / 2)); ctx.fillStyle = THEME.colors.border; ctx.fillRect(weightX, weightY, 32, 1); const weightValue = parseFloat(weight) || 1.0; ctx.fillStyle = THEME.colors.accent; ctx.fillRect(weightX, weightY, 32 * (weightValue/2), 1); ctx.fillStyle = THEME.colors.textDim; ctx.font = `500 8px ${THEME.typography.fonts.primary}`; ctx.textAlign = "right"; ctx.fillText(weightValue.toFixed(1), weightX + 50, weightY + 3); ctx.textAlign = "left"; }, }; const MultiTextNode = { onNodeCreated() { if (this.htmlElement) { this.htmlElement.classList.add('multitext-node'); } this.setupWidgets(); this.activePrompts = parseInt(localStorage.getItem('multitext_prompt_count')) || 2; this.maxPrompts = 20; this.isSinglePromptMode = localStorage.getItem('multitext_single_prompt') === "true"; this.separatorValue = localStorage.getItem('multitext_separator') || ', '; this.tooltipState = { visible: false, text: '', x: 0, y: 0 }; this.currentHoverIndex = -1; this.applyPromptMode(); this.updateNodeSize(); setTimeout(() => this.syncSeparatorWidget(), 100); this.loadFonts(); this.loadSavedTemplates(); this.loadSavedPresets(); // Instance-level onMouseDown to handle control buttons without dragging this.onMouseDown = function(event, pos, ctx) { if (event.button === 0) { const controls = this.isInsideControlButtons(pos[0], pos[1]); if (controls.add) { this.addPrompt(); return true; } else if (controls.remove) { this.removePrompt(); return true; } else if (controls.toggle) { this.isSinglePromptMode = !this.isSinglePromptMode; localStorage.setItem('multitext_single_prompt', this.isSinglePromptMode); this.applyPromptMode(); this.setDirtyCanvas(true); return true; } else if (controls.separator) { const separatorMenu = document.createElement('div'); separatorMenu.classList.add('multitext-separator-menu'); const title = document.createElement('div'); title.textContent = 'Select Separator'; title.classList.add('multitext-separator-title'); separatorMenu.appendChild(title); const menuItems = [ { label: 'Space', value: ' ', icon: '⎵' }, { label: 'Comma', value: ',', icon: ',' }, { label: 'Comma + Space', value: ', ', icon: ', ' }, { label: 'New Line', value: '\\n', icon: '↵' }, { label: 'Custom...', value: 'custom', icon: '✏️' } ]; menuItems.forEach(item => { const menuItem = document.createElement('div'); menuItem.classList.add('multitext-separator-item'); const icon = document.createElement('span'); icon.textContent = item.icon; icon.classList.add('multitext-separator-icon'); const label = document.createElement('span'); label.textContent = item.label; label.classList.add('multitext-separator-label'); menuItem.appendChild(icon); menuItem.appendChild(label); if (this.separatorValue === item.value) { menuItem.classList.add('selected'); } menuItem.onclick = () => { if (item.value === 'custom') { const customInput = document.createElement('input'); customInput.type = 'text'; customInput.classList.add('multitext-separator-custom-input'); customInput.placeholder = 'Enter custom separator...'; customInput.maxLength = 10; customInput.style.cssText = ` color: ${THEME.colors.text}; background: ${THEME.colors.bgActive}; border: 1px solid ${THEME.colors.border}; border-radius: 4px; padding: 6px; margin: 8px 0 4px 0; width: 100%; font-family: ${THEME.typography.fonts.primary}; font-size: 12px; `; customInput.value = this.separatorValue; separatorMenu.innerHTML = ''; separatorMenu.appendChild(title); separatorMenu.appendChild(customInput); customInput.focus(); customInput.onkeydown = (e) => { if (e.key === 'Enter') { this.separatorValue = customInput.value; this.setDirtyCanvas(true); if (app.graph) { app.graph.change(); app.graph.setDirtyCanvas(true); } separatorMenu.remove(); } else if (e.key === 'Escape') { separatorMenu.remove(); } }; } else { this.separatorValue = item.value === '\\n' ? '\n' : item.value; localStorage.setItem('multitext_separator', this.separatorValue); this.setDirtyCanvas(true); if (app.graph) { app.graph.change(); app.graph.setDirtyCanvas(true); } separatorMenu.remove(); } }; separatorMenu.appendChild(menuItem); }); const mouseX = event.clientX || event.pageX || window.event?.clientX || pos[0]; const mouseY = event.clientY || event.pageY || window.event?.clientY || pos[1]; separatorMenu.style.left = `${mouseX}px`; separatorMenu.style.top = `${mouseY + 20}px`; const closeMenu = (e) => { if (!separatorMenu.contains(e.target)) { separatorMenu.remove(); document.removeEventListener('mousedown', closeMenu); } }; document.addEventListener('mousedown', closeMenu); document.body.appendChild(separatorMenu); const menuRect = separatorMenu.getBoundingClientRect(); if (menuRect.right > window.innerWidth) { separatorMenu.style.left = `${window.innerWidth - menuRect.width - 10}px`; } if (menuRect.bottom > window.innerHeight) { separatorMenu.style.top = `${window.innerHeight - menuRect.height - 10}px`; } return true; } const index = this.getHoveredWidgetIndex(pos); if (index !== -1 && index < this.activePrompts) { if (this.isInsideToggle(pos, index)) { this.togglePromptRow(index); return true; } this.showEditDialog(index + 1); return true; } } return false; }.bind(this); }, syncSeparatorWidget() { if (this.widgets) { const separatorWidget = this.widgets.find(w => w.name === "separator"); if (separatorWidget && this.separatorValue) { separatorWidget.value = this.separatorValue; separatorWidget._value = this.separatorValue; this.setWidgetValue(separatorWidget, this.separatorValue); if (separatorWidget.callback) { separatorWidget.callback(this.separatorValue); } this.setDirtyCanvas(true); } } }, loadSavedTemplates() { try { const savedTemplates = localStorage.getItem('multitext_templates'); if (savedTemplates) { const parsedTemplates = JSON.parse(savedTemplates); Object.entries(parsedTemplates).forEach(([category, categoryData]) => { if (THEME.promptTemplates[category]) { THEME.promptTemplates[category].icon = categoryData.icon || THEME.promptTemplates[category].icon; THEME.promptTemplates[category].label = categoryData.label || THEME.promptTemplates[category].label; if (Array.isArray(categoryData.templates)) { const defaultTemplates = THEME.promptTemplates[category].templates || []; const defaultTemplateNames = defaultTemplates.map(t => t.name); const filteredDefaults = defaultTemplates.filter(template => !categoryData.templates.some(t => t.name === template.name) ); const customTemplates = categoryData.templates.map(template => ({ ...template, isCustom: true })); THEME.promptTemplates[category].templates = [ ...filteredDefaults, ...customTemplates ]; } } else { THEME.promptTemplates[category] = { ...categoryData, templates: Array.isArray(categoryData.templates) ? categoryData.templates.map(template => ({ ...template, isCustom: true })) : [] }; } }); console.log('Custom templates loaded successfully'); } } catch (error) { console.error('Error loading saved templates:', error); } }, loadSavedPresets() { try { const savedPresets = localStorage.getItem('multitext_category_presets'); if (savedPresets) { const parsedPresets = JSON.parse(savedPresets); Object.entries(parsedPresets).forEach(([category, categoryData]) => { if (THEME.promptPresets[category] && Array.isArray(categoryData.presets)) { const defaultCategoryPresets = THEME.promptPresets[category].presets || []; const defaultPresetLabels = defaultCategoryPresets.map(p => p.label); const customPresets = categoryData.presets.filter( saved => !defaultPresetLabels.includes(saved.label) ).map(p => ({ ...p, isCustom: true })); THEME.promptPresets[category].presets = [ ...defaultCategoryPresets, ...customPresets ]; } }); console.log('Custom presets loaded successfully'); } } catch (error) { console.error('Error loading saved presets:', error); } }, _createPromptWidgets(index) { const textWidget = ComfyWidgets["STRING"](this, `text${index}`, ["STRING", { multiline: true, visible: false }], app); const weightWidget = ComfyWidgets["FLOAT"](this, `weight${index}`, ["FLOAT", { default: 1.0, min: 0.0, max: 2.0, step: 0.1, visible: false }], app); const labelWidget = ComfyWidgets["STRING"]( this, `label${index}`, ["STRING", { multiline: false, visible: false }], app ); const enabledWidget = ComfyWidgets["BOOLEAN"]( this, `enabled${index}`, ["BOOLEAN", { default: true, visible: false }], app ); [textWidget, weightWidget, labelWidget, enabledWidget].forEach(widgetData => { if (widgetData?.widget) { widgetData.widget.computeSize = () => [0, -4]; widgetData.widget.hidden = true; } }); }, setupWidgets() { for (let i = 1; i <= 20; i++) { this._createPromptWidgets(i); } this.separatorValue = " "; this.isActive = true; this.serialize_widgets = true; }, applyPromptMode() { if (this.isSinglePromptMode) { for (let i = 1; i < this.activePrompts; i++) { const enabledWidget = this.widgets.find(w => w.name === `enabled${i+1}`); if (enabledWidget) enabledWidget.value = false; } } this.setDirtyCanvas(true); }, _onMouseDown_disabled(event, pos, ctx) { if (event.button === 0) { const controls = this.isInsideControlButtons(pos[0], pos[1]); if (controls.add) { this.addPrompt(); return true; } else if (controls.remove) { this.removePrompt(); return true; } else if (controls.toggle) { this.isSinglePromptMode = !this.isSinglePromptMode; localStorage.setItem('multitext_single_prompt', this.isSinglePromptMode); this.applyPromptMode(); this.setDirtyCanvas(true); return true; } else if (controls.separator) { const separatorMenu = document.createElement('div'); separatorMenu.classList.add('multitext-separator-menu'); const title = document.createElement('div'); title.textContent = 'Select Separator'; title.classList.add('multitext-separator-title'); separatorMenu.appendChild(title); const menuItems = [ { label: 'Space', value: ' ', icon: '⎵' }, { label: 'Comma', value: ',', icon: ',' }, { label: 'Comma + Space', value: ', ', icon: ', ' }, { label: 'New Line', value: '\\n', icon: '↵' }, { label: 'Custom...', value: 'custom', icon: '✏️' } ]; menuItems.forEach(item => { const menuItem = document.createElement('div'); menuItem.classList.add('multitext-separator-item'); const icon = document.createElement('span'); icon.textContent = item.icon; icon.classList.add('multitext-separator-icon'); const label = document.createElement('span'); label.textContent = item.label; label.classList.add('multitext-separator-label'); menuItem.appendChild(icon); menuItem.appendChild(label); if (this.separatorValue === item.value) { menuItem.classList.add('selected'); } menuItem.onclick = () => { if (item.value === 'custom') { const customInput = document.createElement('input'); customInput.type = 'text'; customInput.classList.add('multitext-separator-custom-input'); customInput.placeholder = 'Enter custom separator...'; customInput.maxLength = 10; while (separatorMenu.firstChild) { separatorMenu.firstChild.remove(); } const title = document.createElement('div'); title.textContent = 'Custom Separator'; title.classList.add('multitext-separator-title'); separatorMenu.appendChild(title); separatorMenu.appendChild(customInput); const confirmButton = document.createElement('div'); confirmButton.textContent = 'OK'; confirmButton.classList.add('multitext-separator-confirm-button'); separatorMenu.appendChild(confirmButton); customInput.focus(); const applyCustomSeparator = () => { const customValue = customInput.value; if (customValue) { this.separatorValue = customValue; localStorage.setItem('multitext_separator', customValue); this.syncSeparatorWidget(); this.setDirtyCanvas(true); if (app.graph) { app.graph.change(); app.graph.setDirtyCanvas(true); } } separatorMenu.remove(); }; confirmButton.onclick = applyCustomSeparator; customInput.onkeydown = (e) => { if (e.key === 'Enter') { applyCustomSeparator(); } else if (e.key === 'Escape') { separatorMenu.remove(); } }; } else { this.separatorValue = item.value === '\\n' ? '\n' : item.value; localStorage.setItem('multitext_separator', this.separatorValue); this.setDirtyCanvas(true); if (app.graph) { app.graph.change(); app.graph.setDirtyCanvas(true); } separatorMenu.remove(); } }; separatorMenu.appendChild(menuItem); }); const mouseX = event.clientX || event.pageX || window.event?.clientX || pos[0]; const mouseY = event.clientY || event.pageY || window.event?.clientY || pos[1]; separatorMenu.style.left = `${mouseX}px`; separatorMenu.style.top = `${mouseY + 20}px`; const closeMenu = (e) => { if (!separatorMenu.contains(e.target)) { separatorMenu.remove(); document.removeEventListener('mousedown', closeMenu); } }; document.addEventListener('mousedown', closeMenu); document.body.appendChild(separatorMenu); const menuRect = separatorMenu.getBoundingClientRect(); if (menuRect.right > window.innerWidth) { separatorMenu.style.left = `${window.innerWidth - menuRect.width - 10}px`; } if (menuRect.bottom > window.innerHeight) { separatorMenu.style.top = `${window.innerHeight - menuRect.height - 10}px`; } return true; } const index = this.getHoveredWidgetIndex(pos); if (index !== -1 && index < this.activePrompts) { if (this.isInsideToggle(pos, index)) { this.togglePromptRow(index); return true; } this.showEditDialog(index + 1); } } }, isInsideToggle(pos, rowIndex) { const margin = THEME.layout.promptRow.padding; const rowHeight = THEME.layout.promptRow.height; const rowMargin = THEME.layout.promptRow.margin; const y = margin + rowIndex * (rowHeight + rowMargin); const toggleWidth = 30; const toggleHeight = 14; const togglePadding = 6; const x1 = margin + togglePadding; const y1 = y + rowHeight / 2 - toggleHeight / 2; return ( pos[0] >= x1 && pos[0] <= x1 + toggleWidth && pos[1] >= y1 && pos[1] <= y1 + toggleHeight ); }, togglePromptRow(index) { const enabledWidget = this.widgets.find(w => w.name === `enabled${index+1}`); if (this.isSinglePromptMode) { for (let i = 0; i < this.activePrompts; i++) { const widget = this.widgets.find(w => w.name === `enabled${i+1}`); if (widget) widget.value = (i === index); } } else { if (enabledWidget) { enabledWidget.value = !enabledWidget.value; } } this.setDirtyCanvas(true); }, showEditDialog(index) { try { const textWidget = this.widgets.find(w => w.name === `text${index}`); const weightWidget = this.widgets.find(w => w.name === `weight${index}`); const labelWidget = this.widgets.find(w => w.name === `label${index}`); if (!textWidget || !weightWidget || !labelWidget) return; const dialog = DialogUtils.createDialog(THEME, {}); dialog.dataset.promptIndex = index.toString(); const content = document.createDocumentFragment(); const saveTemplateButton = DialogUtils.createButton(THEME, { innerHTML: '✚', title: 'Save as Template', className: 'multitext-dialog-button-savetpl', onclick: (event) => { this.showSaveChoiceDialog(event, dialog.textarea.value, parseFloat(dialog.weightInput.value), dialog); } }); const saveButton = DialogUtils.createButton(THEME, { innerHTML: '✔️', className: 'multitext-dialog-button-save', onclick: () => { labelWidget.value = labelInput.value; textWidget.value = textarea.value; weightWidget.value = parseFloat(weightInput.value); this.saveToHistory(textarea.value); this.updateHistoryDropdown(historyDropdown); removeDragListeners(); dialog.closest('.multitext-dialog')?.remove(); this.setDirtyCanvas(true); } }); const closeBtn = DialogUtils.createButton(THEME, { innerHTML: '×', className: 'multitext-dialog-button-close', onclick: () => dialog.remove() }); const header = DialogUtils.createHeader(THEME, `Edit Prompt ${index}`, [saveTemplateButton, saveButton, closeBtn]); let isDragging = false; let xOffset = 0; let yOffset = 0; const removeDragListeners = () => { document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', dragEnd); }; function dragStart(e) { if (e.target === header || e.target.classList.contains('multitext-dialog-title')) { const popupElement = dialog.closest('.multitext-dialog'); if (!popupElement) return; isDragging = true; popupElement.style.transition = 'none'; header.style.cursor = 'grabbing'; const rect = popupElement.getBoundingClientRect(); xOffset = e.clientX - rect.left; yOffset = e.clientY - rect.top; e.preventDefault(); } } function drag(e) { if (isDragging) { const popupElement = dialog.closest('.multitext-dialog'); if (!popupElement) return; e.preventDefault(); const x = e.clientX - xOffset; const y = e.clientY - yOffset; popupElement.style.left = `${x}px`; popupElement.style.top = `${y}px`; popupElement.style.transform = 'none'; } } function dragEnd(e) { if (isDragging) { const popupElement = dialog.closest('.multitext-dialog'); isDragging = false; header.style.cursor = 'move'; if (popupElement) { } } } header.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); const cleanupDragMain = this._makeDialogDraggable(dialog, header); content.appendChild(header); const contentWrapper = document.createElement('div'); contentWrapper.classList.add('multitext-dialog-content'); content.appendChild(contentWrapper); const labelInput = DialogUtils.createInput(THEME, { value: labelWidget.value || '', placeholder: 'Optional name...', }); dialog.labelInput = labelInput; labelInput.style.marginBottom = '12px'; contentWrapper.appendChild(labelInput); const textarea = DialogUtils.createTextarea(THEME, { value: textWidget.value || '', placeholder: 'Enter prompt text here...', }); dialog.textarea = textarea; contentWrapper.appendChild(textarea); const toolbar = document.createElement('div'); toolbar.classList.add('multitext-dialog-toolbar'); const historyDropdown = document.createElement('div'); historyDropdown.className = 'history-dropdown'; historyDropdown.classList.add('multitext-dialog-history-dropdown'); historyDropdown.style.display = 'none'; THEME.promptTools.quickActions.forEach(action => { const btn = DialogUtils.createButton(THEME, { innerHTML: action.icon, title: action.title, className: 'multitext-dialog-toolbar-button', onclick: () => { if (action.action === 'history') { const newDisplay = historyDropdown.style.display === 'none' ? 'block' : 'none'; historyDropdown.style.display = newDisplay; if (newDisplay === 'block') { this.updateHistoryDropdown(historyDropdown); } } else { this.handleQuickAction(action.action, textarea); } } }); toolbar.appendChild(btn); }); toolbar.appendChild(historyDropdown); const weightGroup = document.createElement('div'); weightGroup.classList.add('multitext-dialog-weight-group'); const weightContainer = document.createElement('div'); weightContainer.classList.add('multitext-dialog-weight-container'); const weightLabel = document.createElement('label'); weightLabel.textContent = 'Weight:'; weightLabel.classList.add('multitext-dialog-weight-label'); const weightInput = document.createElement('input'); weightInput.type = 'number'; weightInput.value = weightWidget.value; weightInput.step = '0.1'; weightInput.min = '0'; weightInput.max = '2'; weightInput.classList.add('multitext-dialog-weight-input'); dialog.weightInput = weightInput; weightContainer.appendChild(weightLabel); weightContainer.appendChild(weightInput); weightGroup.appendChild(weightContainer); weightGroup.appendChild(toolbar); contentWrapper.appendChild(weightGroup); const presetContainer = document.createElement('div'); presetContainer.classList.add('multitext-dialog-preset-container'); presetContainer.style.display = 'flex'; presetContainer.style.flexDirection = 'column'; presetContainer.style.gap = '8px'; presetContainer.style.background = '#1b1b1b'; presetContainer.style.borderRadius = '8px'; presetContainer.style.padding = '8px'; presetContainer.style.margin = '4px 0'; const categoryContainer = document.createElement('div'); categoryContainer.style.display = 'flex'; categoryContainer.style.flexWrap = 'wrap'; categoryContainer.style.gap = '4px'; categoryContainer.style.alignItems = 'center'; const presetsContainer = document.createElement('div'); presetsContainer.style.display = 'none'; presetsContainer.style.flexWrap = 'wrap'; presetsContainer.style.gap = '4px'; presetsContainer.style.marginTop = '6px'; presetsContainer.style.paddingTop = '6px'; presetsContainer.style.borderTop = '1px solid #2a2a2a'; let activeCategory = null; Object.entries(THEME.promptPresets).forEach(([category, categoryData]) => { if (!categoryData || !categoryData.presets) return; const categoryTag = document.createElement('button'); categoryTag.classList.add('multitext-dialog-category-tag'); categoryTag.style.background = '#232323'; categoryTag.style.border = '1px solid #2c2c2c'; categoryTag.style.borderRadius = '4px'; categoryTag.style.color = '#999'; categoryTag.style.padding = '4px 8px'; categoryTag.style.cursor = 'pointer'; categoryTag.style.fontSize = '12px'; categoryTag.style.transition = 'all 0.15s ease'; categoryTag.style.display = 'flex'; categoryTag.style.alignItems = 'center'; categoryTag.style.gap = '4px'; categoryTag.style.minWidth = 'fit-content'; categoryTag.style.whiteSpace = 'nowrap'; categoryTag.title = category.charAt(0).toUpperCase() + category.slice(1); const icon = categoryData.icon || ''; categoryTag.innerHTML = `${icon}`; categoryTag.onmouseover = () => { categoryTag.style.background = '#2a2a2a'; categoryTag.style.borderColor = '#363636'; categoryTag.style.color = '#ccc'; }; categoryTag.onmouseout = () => { if (category !== activeCategory) { categoryTag.style.background = '#232323'; categoryTag.style.borderColor = '#2c2c2c'; categoryTag.style.color = '#999'; } }; categoryTag.onclick = () => { if (activeCategory === category) { presetsContainer.style.display = 'none'; categoryTag.style.background = '#232323'; categoryTag.style.borderColor = '#2c2c2c'; categoryTag.style.color = '#999'; activeCategory = null; } else { presetsContainer.innerHTML = ''; presetsContainer.style.display = 'flex'; categoryContainer.querySelectorAll('button').forEach(btn => { btn.style.background = '#232323'; btn.style.borderColor = '#2c2c2c'; btn.style.color = '#999'; }); categoryTag.style.background = '#2a2a2a'; categoryTag.style.borderColor = '#363636'; categoryTag.style.color = '#ccc'; activeCategory = category; categoryData.presets.forEach(preset => { const presetBtn = document.createElement('button'); presetBtn.classList.add('multitext-dialog-preset-tag'); presetBtn.style.background = '#1e1e1e'; presetBtn.style.border = '1px solid #2c2c2c'; presetBtn.style.borderRadius = '4px'; presetBtn.style.color = '#999'; presetBtn.style.padding = '3px 8px'; presetBtn.style.cursor = 'pointer'; presetBtn.style.fontSize = '12px'; presetBtn.style.transition = 'all 0.15s ease'; presetBtn.style.display = 'flex'; presetBtn.style.alignItems = 'center'; presetBtn.style.gap = '4px'; presetBtn.innerHTML = preset.label; presetBtn.oncontextmenu = (e) => { e.preventDefault(); e.stopPropagation(); const menu = document.createElement('div'); menu.style.position = 'fixed'; menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px'; menu.style.background = '#1b1b1b'; menu.style.border = '1px solid #2c2c2c'; menu.style.borderRadius = '4px'; menu.style.padding = '4px'; menu.style.zIndex = '10000'; const editBtn = document.createElement('button'); editBtn.style.display = 'block'; editBtn.style.width = '100%'; editBtn.style.padding = '4px 8px'; editBtn.style.background = 'none'; editBtn.style.border = 'none'; editBtn.style.color = '#999'; editBtn.style.cursor = 'pointer'; editBtn.style.textAlign = 'left'; editBtn.innerHTML = '✏️ Edit'; editBtn.onmouseover = () => editBtn.style.background = '#252525'; editBtn.onmouseout = () => editBtn.style.background = 'none'; editBtn.onclick = () => { this._showEditPresetDialog(preset); menu.remove(); }; menu.appendChild(editBtn); const deleteBtn = document.createElement('button'); deleteBtn.style.display = 'block'; deleteBtn.style.width = '100%'; deleteBtn.style.padding = '4px 8px'; deleteBtn.style.background = 'none'; deleteBtn.style.border = 'none'; deleteBtn.style.color = '#999'; deleteBtn.style.cursor = 'pointer'; deleteBtn.style.textAlign = 'left'; deleteBtn.innerHTML = '🗑️ Delete'; deleteBtn.onmouseover = () => deleteBtn.style.background = '#252525'; deleteBtn.onmouseout = () => deleteBtn.style.background = 'none'; deleteBtn.onclick = () => { this._showDeletePresetDialog(preset); menu.remove(); }; menu.appendChild(deleteBtn); document.body.appendChild(menu); const closeMenu = (e) => { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', closeMenu); } }; document.addEventListener('click', closeMenu); }; presetBtn.onmouseover = () => { presetBtn.style.background = '#252525'; presetBtn.style.borderColor = '#363636'; presetBtn.style.color = '#ccc'; }; presetBtn.onmouseout = () => { presetBtn.style.background = '#1e1e1e'; presetBtn.style.borderColor = '#2c2c2c'; presetBtn.style.color = '#999'; }; presetBtn.onclick = (e) => { e.stopPropagation(); if (!preset.value) return; const textarea = dialog.textarea; if (textarea) { const currentText = textarea.value; const newText = preset.value; textarea.value = currentText ? `${currentText}, ${newText}` : newText; textarea.dispatchEvent(new Event('input')); const textWidget = this.widgets.find(w => w.name === `text${dialog.dataset.promptIndex}`); if (textWidget) { textWidget.value = textarea.value; } presetBtn.style.background = '#2d2d2d'; setTimeout(() => { presetBtn.style.background = '#1e1e1e'; }, 150); } }; presetsContainer.appendChild(presetBtn); }); } }; categoryContainer.appendChild(categoryTag); }); const editButton = document.createElement('button'); editButton.classList.add('multitext-dialog-category-tag'); editButton.style.background = '#232323'; editButton.style.border = '1px solid #2c2c2c'; editButton.style.borderRadius = '4px'; editButton.style.color = '#999'; editButton.style.padding = '4px 8px'; editButton.style.cursor = 'pointer'; editButton.style.fontSize = '12px'; editButton.style.transition = 'all 0.15s ease'; editButton.style.display = 'flex'; editButton.style.alignItems = 'center'; editButton.style.marginLeft = 'auto'; editButton.title = 'Manage Presets'; editButton.innerHTML = '⚙️'; editButton.onmouseover = () => { editButton.style.background = '#2a2a2a'; editButton.style.borderColor = '#363636'; editButton.style.color = '#ccc'; }; editButton.onmouseout = () => { editButton.style.background = '#232323'; editButton.style.borderColor = '#2c2c2c'; editButton.style.color = '#999'; }; editButton.onclick = () => { const managementDialog = DialogUtils.createDialog(THEME, { width: '320px', maxHeight: '60vh', className: 'multitext-manage-presets-dialog' }); const closeManageDialog = () => { managementDialog.remove(); document.removeEventListener('mousedown', closeDialogOnClickOutside); }; const closeDialogButton = DialogUtils.createButton(THEME, { innerHTML: '✕', className: 'multitext-dialog-button-close', onclick: closeManageDialog }); const header = DialogUtils.createHeader(THEME, 'Manage Presets', [closeDialogButton]); managementDialog.appendChild(header); const contentContainer = document.createElement('div'); contentContainer.style.overflowY = 'auto'; contentContainer.style.overflowX = 'hidden'; contentContainer.style.paddingRight = '8px'; contentContainer.style.marginRight = '-8px'; contentContainer.style.display = 'flex'; contentContainer.style.flexDirection = 'column'; contentContainer.style.gap = '8px'; contentContainer.classList.add('multitext-dialog-content'); Object.entries(THEME.promptPresets).forEach(([category, categoryData]) => { if (!categoryData || !categoryData.presets) return; const categorySection = document.createElement('div'); categorySection.style.display = 'flex'; categorySection.style.flexDirection = 'column'; categorySection.style.gap = '4px'; categorySection.classList.add('multitext-manage-presets-category-section'); const categoryHeader = document.createElement('div'); categoryHeader.style.display = 'flex'; categoryHeader.style.alignItems = 'center'; categoryHeader.style.gap = '6px'; categoryHeader.classList.add('multitext-manage-presets-category-header'); const categoryTitle = document.createElement('h4'); categoryTitle.textContent = category.charAt(0).toUpperCase() + category.slice(1); categoryTitle.style.margin = '0'; categoryTitle.style.fontSize = '13px'; categoryTitle.style.fontWeight = 'normal'; categoryTitle.style.color = '#ccc'; categoryTitle.style.flex = '1'; categoryTitle.classList.add('multitext-manage-presets-category-title'); const addPresetBtn = DialogUtils.createButton(THEME, { innerHTML: '+', title: 'Add new preset', className: 'multitext-manage-presets-add-button', onclick: (e) => { e.stopPropagation(); this.showSavePresetDialog('', 1.0, managementDialog); } }); addPresetBtn.style.padding = '2px 6px'; addPresetBtn.style.fontSize = '12px'; addPresetBtn.style.minWidth = '20px'; const presetsList = document.createElement('div'); presetsList.style.display = 'flex'; presetsList.style.flexDirection = 'column'; presetsList.style.gap = '2px'; presetsList.classList.add('multitext-manage-presets-list'); categoryData.presets.forEach(preset => { const presetItem = document.createElement('div'); presetItem.style.display = 'flex'; presetItem.style.alignItems = 'center'; presetItem.style.gap = '6px'; presetItem.style.padding = '4px 6px'; presetItem.style.background = '#1e1e1e'; presetItem.style.borderRadius = '4px'; presetItem.style.border = '1px solid #2c2c2c'; presetItem.style.fontSize = '12px'; presetItem.classList.add('multitext-manage-presets-item'); const presetLabel = document.createElement('span'); presetLabel.textContent = preset.label; presetLabel.style.flex = '1'; presetLabel.style.overflow = 'hidden'; presetLabel.style.textOverflow = 'ellipsis'; presetLabel.style.whiteSpace = 'nowrap'; presetLabel.style.color = '#999'; presetLabel.classList.add('multitext-manage-presets-item-label'); const buttonsContainer = document.createElement('div'); buttonsContainer.style.display = 'flex'; buttonsContainer.style.gap = '2px'; buttonsContainer.style.alignItems = 'center'; buttonsContainer.classList.add('multitext-manage-presets-item-buttons'); const editPresetBtn = DialogUtils.createButton(THEME, { innerHTML: '✏️', title: 'Edit preset', className: 'multitext-manage-presets-action-button', onclick: (e) => { e.stopPropagation(); const showEditPresetDialog = (preset, presetLabelElement, categorySectionElement, categoryDataObject, presetsListElement) => { const editDialog = DialogUtils.createDialog(THEME, { className: 'multitext-save-preset-dialog', }); editDialog.style.zIndex = '10002'; let isDraggingEdit = false; let xOffsetEdit = 0; let yOffsetEdit = 0; const removeDragListenersEdit = () => { document.removeEventListener('mousemove', dragEdit); document.removeEventListener('mouseup', dragEndEdit); }; function dragStartEdit(e) { if (e.target === editHeader || editHeader.contains(e.target)) { const popupElement = editDialog; if (!popupElement) return; isDraggingEdit = true; popupElement.style.transition = 'none'; editHeader.style.cursor = 'grabbing'; const rect = popupElement.getBoundingClientRect(); xOffsetEdit = e.clientX - rect.left; yOffsetEdit = e.clientY - rect.top; e.preventDefault(); document.addEventListener('mousemove', dragEdit); document.addEventListener('mouseup', dragEndEdit); } } function dragEdit(e) { if (isDraggingEdit) { const popupElement = editDialog; if (!popupElement) return; e.preventDefault(); const x = e.clientX - xOffsetEdit; const y = e.clientY - yOffsetEdit; popupElement.style.left = `${x}px`; popupElement.style.top = `${y}px`; popupElement.style.transform = 'none'; } } function dragEndEdit(e) { if (isDraggingEdit) { isDraggingEdit = false; editHeader.style.cursor = 'move'; removeDragListenersEdit(); } } const editHeader = DialogUtils.createHeader(THEME, 'Edit Preset', [ DialogUtils.createButton(THEME, { innerHTML: '✕', className: 'multitext-dialog-button-close', onclick: () => editDialog.remove() }) ]); editDialog.appendChild(editHeader); editHeader.addEventListener('mousedown', dragStartEdit); const cleanupDragEdit = this._makeDialogDraggable(editDialog, editHeader); const nameInput = DialogUtils.createInput(THEME, { value: preset.label, placeholder: 'Preset name', className: 'multitext-save-preset-input' }); editDialog.appendChild(nameInput); const valueInput = DialogUtils.createTextarea(THEME, { value: preset.value, placeholder: 'Preset value', className: 'multitext-dialog-textarea' }); editDialog.appendChild(valueInput); const buttons = document.createElement('div'); buttons.classList.add('multitext-save-preset-buttons'); const cancelBtn = DialogUtils.createButton(THEME, { innerHTML: 'Cancel', className: 'multitext-save-preset-button multitext-save-preset-button-cancel', onclick: (e) => { e.stopPropagation(); editDialog.remove(); } }); const saveBtn = DialogUtils.createButton(THEME, { innerHTML: 'Save', className: 'multitext-save-preset-button multitext-save-preset-button-save', onclick: (e) => { e.stopPropagation(); const newName = nameInput.value.trim(); const newValue = valueInput.value.trim(); if (newName && newValue) { preset.label = newName; preset.value = newValue; presetLabelElement.textContent = newName; this.saveToHistory(newValue); localStorage.setItem('multitext_category_presets', JSON.stringify(THEME.promptPresets)); editDialog.remove(); } } }); buttons.appendChild(cancelBtn); buttons.appendChild(saveBtn); editDialog.appendChild(buttons); editDialog.addEventListener('mousedown', (e) => { e.stopPropagation(); }); document.body.appendChild(editDialog); nameInput.focus(); nameInput.select(); requestAnimationFrame(() => { const container = document.querySelector('.graph-canvas-container') || document.body; const containerRect = container.getBoundingClientRect(); const dialogRect = editDialog.getBoundingClientRect(); let desiredLeft = (containerRect.width - dialogRect.width) / 2 + containerRect.left; let desiredTop = (containerRect.height - dialogRect.height) / 2 + containerRect.top; const clampedLeft = Math.max( containerRect.left + 10, Math.min(containerRect.right - dialogRect.width - 10, desiredLeft) ); const clampedTop = Math.max( containerRect.top + 10, Math.min(containerRect.bottom - dialogRect.height - 10, desiredTop) ); editDialog.style.left = clampedLeft + 'px'; editDialog.style.top = clampedTop + 'px'; editDialog.style.transform = 'none'; }); }; showEditPresetDialog(preset, presetLabel, categorySection, categoryData, presetsList); } }); editPresetBtn.style.background = 'none'; editPresetBtn.style.border = 'none'; editPresetBtn.style.padding = '2px'; editPresetBtn.style.opacity = '0.7'; editPresetBtn.style.fontSize = '11px'; const deletePresetBtn = DialogUtils.createButton(THEME, { innerHTML: '🗑️', title: 'Delete preset', className: 'multitext-manage-presets-action-button delete', onclick: (e) => { e.stopPropagation(); if (confirm(`Are you sure you want to delete the preset \"${preset.label}\"?`)) { const presetIndex = categoryData.presets.indexOf(preset); if (presetIndex > -1) { categoryData.presets.splice(presetIndex, 1); localStorage.setItem('multitext_category_presets', JSON.stringify(THEME.promptPresets)); presetItem.remove(); if (categoryData.presets.length === 0) { categorySection.remove(); } } } this._showDeletePresetDialog(preset); const presetItemElement = deleteBtn.closest('.multitext-manage-presets-item'); if (presetItemElement) presetItemElement.remove(); if (categoryData.presets.length === 0) { const categorySectionElement = deleteBtn.closest('.multitext-manage-presets-category-section'); if (categorySectionElement) categorySectionElement.remove(); } } }); deletePresetBtn.style.background = 'none'; deletePresetBtn.style.border = 'none'; deletePresetBtn.style.padding = '2px'; deletePresetBtn.style.opacity = '0.7'; deletePresetBtn.style.fontSize = '11px'; [editPresetBtn, deletePresetBtn].forEach(btn => { btn.onmouseover = () => btn.style.opacity = '1'; btn.onmouseout = () => btn.style.opacity = '0.7'; }); buttonsContainer.appendChild(editPresetBtn); buttonsContainer.appendChild(deletePresetBtn); presetItem.appendChild(presetLabel); presetItem.appendChild(buttonsContainer); presetsList.appendChild(presetItem); }); categoryHeader.appendChild(categoryTitle); categoryHeader.appendChild(addPresetBtn); categorySection.appendChild(categoryHeader); categorySection.appendChild(presetsList); contentContainer.appendChild(categorySection); }); managementDialog.appendChild(contentContainer); document.body.appendChild(managementDialog); requestAnimationFrame(() => { const container = document.querySelector('.graph-canvas-container') || document.body; const containerRect = container.getBoundingClientRect(); const dialogRect = managementDialog.getBoundingClientRect(); let desiredLeft = (containerRect.width - dialogRect.width) / 2 + containerRect.left; let desiredTop = (containerRect.height - dialogRect.height) / 2 + containerRect.top; const clampedLeft = Math.max( containerRect.left, Math.min(containerRect.right - dialogRect.width, desiredLeft) ); const clampedTop = Math.max( containerRect.top, Math.min(containerRect.bottom - dialogRect.height, desiredTop) ); managementDialog.style.left = clampedLeft + 'px'; managementDialog.style.top = clampedTop + 'px'; managementDialog.style.transform = 'none'; }); const closeDialogOnClickOutside = (e) => { if (!managementDialog.contains(e.target)) { closeManageDialog(); } }; document.addEventListener('mousedown', closeDialogOnClickOutside); }; categoryContainer.appendChild(editButton); presetContainer.appendChild(categoryContainer); presetContainer.appendChild(presetsContainer); contentWrapper.appendChild(presetContainer); const templateSection = document.createElement('div'); templateSection.classList.add('multitext-dialog-template-section'); const templateHeader = document.createElement('div'); templateHeader.classList.add('multitext-dialog-template-header'); const templateTitle = document.createElement('div'); templateTitle.innerHTML = '📝 Ready Templates'; templateTitle.classList.add('multitext-dialog-template-title'); const toggleIcon = document.createElement('div'); toggleIcon.textContent = '▼'; toggleIcon.classList.add('multitext-dialog-template-toggle'); templateHeader.appendChild(templateTitle); templateHeader.appendChild(toggleIcon); templateSection.appendChild(templateHeader); const templateGrid = document.createElement('div'); templateGrid.classList.add('multitext-dialog-template-grid'); let isTemplatesVisible = false; const toggleTemplates = () => { isTemplatesVisible = !isTemplatesVisible; toggleIcon.classList.toggle('rotated', isTemplatesVisible); templateGrid.classList.toggle('visible', isTemplatesVisible); }; templateHeader.onclick = toggleTemplates; Object.entries(THEME.promptTemplates).forEach(([category, { icon, label, templates }]) => { if (!Array.isArray(templates)) { console.warn(`Templates for category '${category}' is not an array. Skipping.`); return; } templates.forEach(template => { const templateBtn = document.createElement('button'); templateBtn.classList.add('multitext-dialog-template-button'); const templateBtnHeader = document.createElement('div'); templateBtnHeader.classList.add('multitext-dialog-template-button-header'); templateBtnHeader.innerHTML = `${icon || '📝'} ${template.name}`; const templateInfo = document.createElement('div'); templateInfo.classList.add('multitext-dialog-template-button-info'); templateInfo.innerHTML = ` ${label || category} ${template.weight ? template.weight.toFixed(1) : '-'} `; const editIcon = document.createElement('div'); editIcon.classList.add('multitext-template-edit-icon'); editIcon.innerHTML = '⋮'; editIcon.title = 'Edit template'; editIcon.onclick = (e) => { e.stopPropagation(); this.showSaveTemplateDialog( template.prompt, template.weight, dialog, template.name, category ); }; templateBtn.appendChild(editIcon); templateBtn.appendChild(templateBtnHeader); templateBtn.appendChild(templateInfo); templateBtn.onclick = () => { textarea.value = template.prompt; if (template.weight !== undefined) { weightInput.value = template.weight; } textarea.dispatchEvent(new Event('input')); const textWidget = this.widgets.find(w => w.name === `text${dialog.dataset.promptIndex}`); const weightWidget = this.widgets.find(w => w.name === `weight${dialog.dataset.promptIndex}`); if (textWidget) { textWidget.value = textarea.value; } if (weightWidget && template.weight !== undefined) { weightWidget.value = parseFloat(weightInput.value); } }; templateGrid.appendChild(templateBtn); }); }); templateSection.appendChild(templateGrid); contentWrapper.appendChild(templateSection); dialog.appendChild(content); document.body.appendChild(dialog); const container = document.querySelector('.graph-canvas-container'); const containerRect = container ? container.getBoundingClientRect() : { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight }; const dialogRect = dialog.getBoundingClientRect(); let desiredLeft = (window.innerWidth - dialogRect.width) / 2; let desiredTop = (window.innerHeight - dialogRect.height) / 2; const clampedLeft = Math.max( containerRect.left, Math.min(containerRect.right - dialogRect.width, desiredLeft) ); const clampedTop = Math.max( containerRect.top, Math.min(containerRect.bottom - dialogRect.height, desiredTop) ); dialog.style.left = clampedLeft + 'px'; dialog.style.top = clampedTop + 'px'; dialog.style.transform = 'none'; textarea.focus(); dialog.textarea = textarea; dialog.weightInput = weightInput; } catch (error) { console.error('Dialog creation error:', error); } }, onDrawForeground(ctx) { if (!ctx || this.flags.collapsed) return; const prLayout = THEME.layout.promptRow; const margin = prLayout.padding; const rowHeight = prLayout.height; const rowMargin = prLayout.margin; const rowWidth = this.size[0] - (margin * 2); let y = margin; for (let i = 0; i < this.activePrompts; i++) { const labelWidget = this.widgets.find(w => w.name === `label${i+1}`); const textWidget = this.widgets.find(w => w.name === `text${i + 1}`); const weightWidget = this.widgets.find(w => w.name === `weight${i + 1}`); const enabledWidget = this.widgets.find(w => w.name === `enabled${i+1}`); if (labelWidget && textWidget && weightWidget) { if (i === this.currentHoverIndex) { UXUtils.drawHoverEffect( ctx, margin, y, this.size[0] - (margin * 2), rowHeight, 1 ); } Renderer.drawPromptRow( ctx, margin, y, labelWidget.value || textWidget.value || `Prompt ${i + 1}`, weightWidget.value, rowWidth, rowHeight, 4 ); this.drawToggle( ctx, margin + 4, y + rowHeight/2, enabledWidget.value ); y += rowHeight + rowMargin; } } this.drawControlButtons(ctx); if (this.tooltipState.visible) { UXUtils.drawTooltip( ctx, this.tooltipState.text, this.tooltipState.x, this.tooltipState.y ); } }, drawToggle(ctx, x, centerY, isEnabled) { const size = 10; const toggleX = x; const toggleY = centerY - size/2; ctx.save(); ctx.fillStyle = isEnabled ? THEME.colors.accent : THEME.colors.bg; ctx.strokeStyle = isEnabled ? THEME.colors.accent : THEME.colors.border; ctx.lineWidth = 1; ctx.beginPath(); ctx.roundRect(toggleX, toggleY, size, size, 2); ctx.fill(); if (!isEnabled) ctx.stroke(); if (isEnabled) { ctx.fillStyle = THEME.colors.text; ctx.beginPath(); ctx.arc( toggleX + size/2, toggleY + size/2, size/4, 0, Math.PI * 2 ); ctx.fill(); } ctx.restore(); }, getHoveredWidgetIndex(pos) { const prLayout = THEME.layout.promptRow; const margin = prLayout.padding; const rowHeight = prLayout.height; const rowMargin = prLayout.margin; const relativeY = pos[1] - margin; const rowIndex = Math.floor(relativeY / (rowHeight + rowMargin)); if (rowIndex >= 0 && rowIndex < this.activePrompts && pos[0] >= margin && pos[0] <= this.size[0] - margin && relativeY >= 0 && relativeY <= this.activePrompts * (rowHeight + rowMargin)) { return rowIndex; } return -1; }, loadPromptHistory() { try { const history = localStorage.getItem('multitext_history'); return history ? JSON.parse(history) : []; } catch (error) { console.error('Error loading history:', error); return []; } }, saveToHistory(text) { if (!text?.trim()) return; try { const history = this.loadPromptHistory(); const newItem = { text, timestamp: Date.now() }; const existingIndex = history.findIndex(item => item.text === text); if (existingIndex !== -1) { history[existingIndex] = newItem; } else { history.unshift(newItem); } const trimmedHistory = history.slice(0, 20); localStorage.setItem('multitext_history', JSON.stringify(trimmedHistory)); } catch (error) { console.error('Error saving to history:', error); } }, handleQuickAction(action, textarea) { switch (action) { case 'weight': const weightInput = document.querySelector('.multitext-dialog-weight-input'); if (weightInput) { weightInput.focus(); weightInput.select(); } break; case 'clear': if (textarea.value) { textarea.value = ''; textarea.dispatchEvent(new Event('input')); } break; case 'default': if (confirm("This will remove all custom presets and templates and restore defaults. Are you sure?")) { localStorage.removeItem('multitext_category_presets'); localStorage.removeItem('multitext_templates'); alert("Custom presets and templates cleared. Please reload the UI or node for changes to take full effect."); const dialog = document.querySelector(`.multitext-dialog[data-prompt-index="${textarea.closest('.multitext-dialog')?.dataset.promptIndex}"]`); dialog?.remove(); } break; } }, addPrompt() { if (this.activePrompts < this.maxPrompts) { this.activePrompts++; const newEnabledWidget = this.widgets.find(w => w.name === `enabled${this.activePrompts}`); if (newEnabledWidget) { newEnabledWidget.value = false; } localStorage.setItem('multitext_prompt_count', this.activePrompts.toString()); this.updateNodeSize(); this.setDirtyCanvas(true); } }, removePrompt() { if (this.activePrompts > 1) { const index = this.activePrompts; const textWidget = this.widgets.find(w => w.name === `text${index}`); const weightWidget = this.widgets.find(w => w.name === `weight${index}`); if (textWidget) textWidget.value = ""; if (weightWidget) weightWidget.value = 1.0; this.activePrompts--; localStorage.setItem('multitext_prompt_count', this.activePrompts.toString()); this.updateNodeSize(); this.setDirtyCanvas(true); } }, isInsideControlButtons(x, y) { const prLayout = THEME.layout.promptRow; const margin = prLayout.padding; const buttonSize = THEME.controls.button.size; const buttonMargin = THEME.controls.button.margin; const controlsY = margin + (this.activePrompts * (prLayout.height + prLayout.margin)) + buttonMargin; const removeX = margin; const addX = removeX + buttonSize + 4; const separatorX = addX + buttonSize + 4; const toggleX = this.size[0] - margin - 80; const toggleWidth = 80; const toggleHeight = 20; const toggleY = controlsY - 2; return { remove: x >= removeX && x <= removeX + buttonSize && y >= controlsY && y <= controlsY + buttonSize, add: x >= addX && x <= addX + buttonSize && y >= controlsY && y <= controlsY + buttonSize, separator: x >= separatorX && x <= separatorX + buttonSize && y >= controlsY && y <= controlsY + buttonSize, toggle: x >= toggleX && x <= toggleX + toggleWidth && y >= toggleY && y <= toggleY + toggleHeight }; }, drawControlButtons(ctx) { const prLayout = THEME.layout.promptRow; const margin = prLayout.padding; const buttonSize = THEME.controls.button.size; const buttonMargin = THEME.controls.button.margin; const y = margin + (this.activePrompts * (prLayout.height + prLayout.margin)) + buttonMargin; const removeX = margin; const addX = removeX + buttonSize + 4; const separatorX = addX + buttonSize + 4; const toggleX = this.size[0] - margin - 60; ctx.fillStyle = this.activePrompts > 1 ? THEME.controls.button.colors.bg : THEME.controls.button.colors.bgDisabled; Utils.drawRoundedRect(ctx, removeX, y, buttonSize, buttonSize, THEME.controls.button.borderRadius); ctx.strokeStyle = this.activePrompts > 1 ? THEME.controls.button.colors.text : THEME.controls.button.colors.textDisabled; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(removeX + 6, y + buttonSize/2); ctx.lineTo(removeX + buttonSize - 6, y + buttonSize/2); ctx.stroke(); ctx.fillStyle = this.activePrompts < this.maxPrompts ? THEME.controls.button.colors.bg : THEME.controls.button.colors.bgDisabled; Utils.drawRoundedRect(ctx, addX, y, buttonSize, buttonSize, THEME.controls.button.borderRadius); ctx.strokeStyle = this.activePrompts < this.maxPrompts ? THEME.controls.button.colors.text : THEME.controls.button.colors.textDisabled; ctx.beginPath(); ctx.moveTo(addX + 6, y + buttonSize/2); ctx.lineTo(addX + buttonSize - 6, y + buttonSize/2); ctx.moveTo(addX + buttonSize/2, y + 6); ctx.lineTo(addX + buttonSize/2, y + buttonSize - 6); ctx.stroke(); ctx.fillStyle = THEME.controls.button.colors.bg; Utils.drawRoundedRect(ctx, separatorX, y, buttonSize, buttonSize, THEME.controls.button.borderRadius); ctx.fillStyle = THEME.controls.button.colors.text; ctx.font = '16px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('⋮', separatorX + buttonSize/2, y + buttonSize/2); this.drawToggleSwitch(ctx, toggleX, y, buttonSize, this.isSinglePromptMode); }, drawToggleSwitch(ctx, x, y, size, isSingleMode) { const width = 28; const height = 16; const radius = height / 2; const offset = 30; const toggleX = x + offset; const toggleY = y + (size - height) / 2; ctx.save(); ctx.font = `600 6px ${THEME.typography.fonts.primary}`; ctx.textBaseline = "middle"; ctx.textAlign = "right"; ctx.fillStyle = THEME.colors.text; ctx.fillText(isSingleMode ? "Single" : "Multi", toggleX - 4, toggleY + height/2); ctx.beginPath(); ctx.fillStyle = isSingleMode ? `rgba(${parseInt(THEME.colors.accent.slice(1,3),16)}, ${parseInt(THEME.colors.accent.slice(3,5),16)}, ${parseInt(THEME.colors.accent.slice(5,7),16)}, 0.15)` : 'rgba(45, 45, 45, 0.2)'; ctx.roundRect(toggleX, toggleY, width, height, radius); ctx.fill(); ctx.strokeStyle = isSingleMode ? THEME.colors.accent : THEME.colors.border; ctx.lineWidth = 1; ctx.stroke(); const handleRadius = height / 2 - 3; const handleX = isSingleMode ? toggleX + width - (handleRadius * 2) - 3 : toggleX + 3; const handleY = toggleY + 3; ctx.shadowColor = 'rgba(0, 0, 0, 0.1)'; ctx.shadowBlur = 2; ctx.shadowOffsetY = 1; const handleGradient = ctx.createLinearGradient(handleX, handleY, handleX, handleY + handleRadius * 2); handleGradient.addColorStop(0, isSingleMode ? THEME.colors.accent : '#FFFFFF'); handleGradient.addColorStop(1, isSingleMode ? THEME.colors.accentLight : '#F0F0F0'); ctx.beginPath(); ctx.fillStyle = handleGradient; ctx.arc(handleX + handleRadius, handleY + handleRadius, handleRadius, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = isSingleMode ? THEME.colors.accentLight : THEME.colors.border; ctx.lineWidth = 0.5; ctx.stroke(); ctx.restore(); }, updateNodeSize() { const prLayout = THEME.layout.promptRow; const margin = prLayout.padding; const rowHeight = prLayout.height; const rowMargin = prLayout.margin; const buttonSize = THEME.controls.button.size; const buttonMargin = THEME.controls.button.margin; const totalHeight = margin + (this.activePrompts * (rowHeight + rowMargin)) + buttonMargin + buttonSize + margin; this.size[0] = Math.max(THEME.layout.minNodeWidth, this.size[0]); this.size[1] = totalHeight; this.setDirtyCanvas(true); }, onResize(size) { const prLayout = THEME.layout.promptRow; const margin = prLayout.padding; const rowHeight = prLayout.height; const rowMargin = prLayout.margin; const buttonSize = THEME.controls.button.size; const buttonMargin = THEME.controls.button.margin; const minHeight = margin + (this.activePrompts * (rowHeight + rowMargin)) + buttonMargin + buttonSize + margin; size[0] = Math.max(THEME.layout.minNodeWidth, size[0]); size[1] = minHeight; return size; }, onSerialize(o) { if (!o.widgets_values) { o.widgets_values = []; } o.active_prompts = this.activePrompts; o.separator_value = this.separatorValue; }, onConfigure(o) { if (o.active_prompts !== undefined) { this.activePrompts = o.active_prompts; localStorage.setItem('multitext_prompt_count', this.activePrompts.toString()); } if (o.separator_value !== undefined) { this.separatorValue = o.separator_value; localStorage.setItem('multitext_separator', o.separator_value); setTimeout(() => this.syncSeparatorWidget(), 100); } this.updateNodeSize(); }, onMouseMove(event, pos, ctx) { const index = this.getHoveredWidgetIndex(pos); if (!app.ui.settings.getSettingValue("SKB.ShowTooltips")) { this.tooltipState.visible = false; this.setDirtyCanvas(true); return; } if (this.currentHoverIndex === index) { if (this.tooltipState.visible) { const canvasRect = ctx.canvas.getBoundingClientRect(); this.tooltipState.x = event.clientX - canvasRect.left; this.tooltipState.y = event.clientY - canvasRect.top; this.setDirtyCanvas(true); } return; } this.currentHoverIndex = index; const textWidget = index !== -1 ? this.widgets.find(w => w.name === `text${index + 1}`) : null; this.tooltipState = { visible: textWidget?.value ? true : false, text: textWidget?.value || '', x: event.clientX - ctx.canvas.getBoundingClientRect().left, y: event.clientY - ctx.canvas.getBoundingClientRect().top }; this.setDirtyCanvas(true); }, showSavePresetDialog(text, weight, dialogToUpdate) { const dialog = DialogUtils.createDialog(THEME, { width: '400px', maxHeight: '500px' }); const closeButton = DialogUtils.createButton(THEME, { icon: '', title: 'Close', onclick: () => dialog.remove() }); const header = DialogUtils.createHeader(THEME, 'Save Preset', [closeButton]); dialog.appendChild(header); const content = document.createElement('div'); content.classList.add('multitext-dialog-content'); content.style.padding = '16px'; const nameInput = DialogUtils.createInput(THEME, { placeholder: 'Optional preset name', className: 'multitext-save-preset-input' }); nameInput.style.marginBottom = '12px'; const valueTextarea = DialogUtils.createTextarea(THEME, { placeholder: 'Preset value (required)', value: text || '', className: 'multitext-dialog-textarea' }); valueTextarea.style.marginBottom = '12px'; valueTextarea.addEventListener('input', () => { if (!nameInput.value.trim()) { const firstLine = valueTextarea.value.split('\n')[0].trim(); const truncated = firstLine.length > 40 ? firstLine.substring(0, 37) + '...' : firstLine; nameInput.placeholder = truncated || 'Optional preset name'; } }); const categorySelect = DialogUtils.createSelect(THEME, { className: 'multitext-save-preset-select', options: [ { value: 'quality', label: '✨ Quality' }, { value: 'style', label: '🎨 Style' }, { value: 'lighting', label: '💡 Lighting' }, { value: 'camera', label: '📷 Camera' }, { value: 'mood', label: '🎭 Mood' }, { value: 'environment', label: '🌍 Environment' } ] }); content.appendChild(nameInput); content.appendChild(valueTextarea); content.appendChild(categorySelect); const buttons = document.createElement('div'); buttons.classList.add('multitext-save-preset-buttons'); const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'Cancel'; cancelBtn.classList.add('multitext-save-preset-button', 'multitext-save-preset-button-cancel'); cancelBtn.onclick = () => dialog.remove(); const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.classList.add('multitext-save-preset-button', 'multitext-save-preset-button-save'); saveBtn.onclick = async () => { const value = valueTextarea.value.trim(); if (!value) { alert("Please enter a preset value."); return; } const name = nameInput.value.trim() || value.split('\n')[0].trim(); const category = categorySelect.value; if (!category) { alert("Please select a category."); return; } try { if (!THEME.promptPresets[category]) { THEME.promptPresets[category] = { presets: [] }; } else if (!Array.isArray(THEME.promptPresets[category].presets)) { THEME.promptPresets[category].presets = []; } THEME.promptPresets[category].presets = THEME.promptPresets[category].presets.filter(preset => preset.label !== name ); THEME.promptPresets[category].presets.push({ value: valueTextarea.value.trim(), label: name, weight: 1.0 }); try { localStorage.setItem('multitext_category_presets', JSON.stringify(THEME.promptPresets)); console.log(`Preset \"${name}\" successfully saved to ${category} category`); dialog.remove(); } catch (error) { console.error('Error saving preset:', error); alert('Failed to save preset. Please try again.'); dialog.remove(); } } catch (error) { console.error('Error saving preset:', error); alert('Failed to save preset. Please try again.'); dialog.remove(); } }; buttons.appendChild(cancelBtn); buttons.appendChild(saveBtn); content.appendChild(buttons); nameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { saveBtn.click(); } }); dialog.appendChild(content); document.body.appendChild(dialog); const cleanupDragSavePreset = this._makeDialogDraggable(dialog, header); requestAnimationFrame(() => { const container = document.querySelector('.graph-canvas-container') || document.body; const containerRect = container.getBoundingClientRect(); const dialogRect = dialog.getBoundingClientRect(); let desiredLeft = (containerRect.width - dialogRect.width) / 2 + containerRect.left; let desiredTop = (containerRect.height - dialogRect.height) / 2 + containerRect.top; const clampedLeft = Math.max( containerRect.left + 10, Math.min(containerRect.right - dialogRect.width - 10, desiredLeft) ); const clampedTop = Math.max( containerRect.top + 10, Math.min(containerRect.bottom - dialogRect.height - 10, desiredTop) ); dialog.style.left = clampedLeft + 'px'; dialog.style.top = clampedTop + 'px'; dialog.style.transform = 'none'; }); nameInput.focus(); }, showSaveTemplateDialog(promptText, promptWeight, dialogToUpdate, existingName = '', existingCategory = '') { const dialog = DialogUtils.createDialog(THEME, {}); dialog.classList.remove('multitext-dialog'); dialog.classList.add('multitext-save-preset-dialog'); const title = document.createElement('div'); title.textContent = existingName ? 'Edit Template' : 'Save Template'; title.classList.add('multitext-save-preset-title'); dialog.appendChild(title); const nameInput = DialogUtils.createInput(THEME, { placeholder: 'Template name', value: existingName, className: 'multitext-save-preset-input' }); dialog.appendChild(nameInput); const promptTextarea = DialogUtils.createTextarea(THEME, { value: promptText || '', placeholder: 'Enter prompt text...', className: 'multitext-dialog-textarea' }); dialog.appendChild(promptTextarea); const categorySelect = DialogUtils.createSelect(THEME, { className: 'multitext-save-preset-select', options: Object.entries(THEME.promptTemplates).map(([key, cat]) => ({ value: key, label: `${cat.icon} ${cat.label || key}`, selected: existingCategory === key })) }); dialog.appendChild(categorySelect); const buttons = document.createElement('div'); buttons.classList.add('multitext-save-preset-buttons'); const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'Cancel'; cancelBtn.classList.add('multitext-save-preset-button', 'multitext-save-preset-button-cancel'); cancelBtn.onclick = () => dialog.remove(); const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.classList.add('multitext-save-preset-button', 'multitext-save-preset-button-save'); saveBtn.onclick = async () => { const name = nameInput.value.trim(); const category = categorySelect.value; const finalPromptText = promptTextarea.value.trim(); if (!name || !finalPromptText || !category) { alert("Please fill in all fields."); return; } try { if (!THEME.promptTemplates[category].templates) { THEME.promptTemplates[category].templates = []; } if (existingName && existingCategory) { if (existingCategory !== category && THEME.promptTemplates[existingCategory]?.templates) { THEME.promptTemplates[existingCategory].templates = THEME.promptTemplates[existingCategory].templates.filter(template => template.name !== existingName ); } THEME.promptTemplates[category].templates = THEME.promptTemplates[category].templates.filter(template => !(template.name === existingName && (existingCategory !== category || template.name === name)) ); } else { THEME.promptTemplates[category].templates = THEME.promptTemplates[category].templates.filter(template => template.name !== name ); } THEME.promptTemplates[category].templates.push({ name: name, prompt: finalPromptText, weight: promptWeight, isCustom: true }); localStorage.setItem('multitext_templates', JSON.stringify(THEME.promptTemplates)); console.log(`Template \"${name}\" successfully ${existingName ? 'updated' : 'saved'} to ${category} category`); dialog.remove(); if (dialogToUpdate) { setTimeout(() => { this.updateTemplateGrid(dialogToUpdate); }, 100); } } catch (error) { console.error('Error saving template:', error); alert('Failed to save template. Please try again.'); dialog.remove(); } }; if (existingName && existingCategory) { const deleteBtn = document.createElement('button'); deleteBtn.innerHTML = '✖'; deleteBtn.title = 'Delete template'; deleteBtn.classList.add('multitext-save-preset-button', 'multitext-save-preset-button-delete'); deleteBtn.style.backgroundColor = '#EF4444'; deleteBtn.style.color = 'white'; deleteBtn.style.borderColor = '#B91C1C'; deleteBtn.style.minWidth = '40px'; deleteBtn.style.fontSize = '16px'; deleteBtn.style.padding = '5px 10px'; deleteBtn.onclick = () => { if (confirm(`Are you sure you want to delete the template \"${existingName}\"?`)) { if (THEME.promptTemplates[existingCategory]?.templates) { THEME.promptTemplates[existingCategory].templates = THEME.promptTemplates[existingCategory].templates.filter(template => template.name !== existingName ); localStorage.setItem('multitext_templates', JSON.stringify(THEME.promptTemplates)); console.log(`Template \"${existingName}\" successfully deleted from ${existingCategory} category`); if (dialogToUpdate) { this.updateTemplateGrid(dialogToUpdate); } dialog.remove(); } } }; cancelBtn.style.backgroundColor = 'transparent'; cancelBtn.style.color = THEME.colors.text; cancelBtn.style.borderColor = THEME.colors.border; cancelBtn.style.borderRadius = '6px'; cancelBtn.style.padding = '10px 16px'; cancelBtn.style.fontSize = '14px'; cancelBtn.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)'; cancelBtn.style.fontWeight = '500'; saveBtn.style.backgroundColor = '#8B5CF6'; saveBtn.style.color = 'white'; saveBtn.style.borderColor = 'transparent'; saveBtn.style.borderRadius = '6px'; saveBtn.style.padding = '10px 16px'; saveBtn.style.fontSize = '14px'; saveBtn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; saveBtn.style.fontWeight = '500'; buttons.style.display = 'flex'; buttons.style.gap = '12px'; buttons.style.paddingTop = '20px'; const rightButtons = document.createElement('div'); rightButtons.style.display = 'flex'; rightButtons.style.gap = '12px'; rightButtons.style.marginLeft = 'auto'; buttons.appendChild(deleteBtn); rightButtons.appendChild(cancelBtn); rightButtons.appendChild(saveBtn); buttons.appendChild(rightButtons); dialog.appendChild(buttons); } else { cancelBtn.style.backgroundColor = 'transparent'; cancelBtn.style.color = THEME.colors.text; cancelBtn.style.borderColor = THEME.colors.border; cancelBtn.style.borderRadius = '6px'; cancelBtn.style.padding = '10px 16px'; cancelBtn.style.fontSize = '14px'; cancelBtn.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)'; cancelBtn.style.fontWeight = '500'; saveBtn.style.backgroundColor = '#8B5CF6'; saveBtn.style.color = 'white'; saveBtn.style.borderColor = 'transparent'; saveBtn.style.borderRadius = '6px'; saveBtn.style.padding = '10px 16px'; saveBtn.style.fontSize = '14px'; saveBtn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; saveBtn.style.fontWeight = '500'; buttons.style.display = 'flex'; buttons.style.justifyContent = 'flex-end'; buttons.style.gap = '12px'; buttons.style.paddingTop = '20px'; buttons.appendChild(cancelBtn); buttons.appendChild(saveBtn); dialog.appendChild(buttons); } nameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { saveBtn.click(); } }); document.body.appendChild(dialog); nameInput.focus(); dialog.style.left = `calc(50% - ${dialog.offsetWidth / 2}px)`; dialog.style.top = `calc(50% - ${dialog.offsetHeight / 2}px)`; }, showSaveChoiceDialog(event, textToSave, weightToSave, mainDialog) { const choiceDialog = DialogUtils.createDialog(THEME, { minWidth: '200px' }); choiceDialog.className = 'multitext-save-choice-dialog'; const title = document.createElement('div'); title.className = 'multitext-save-choice-title'; title.textContent = 'Save As...'; choiceDialog.appendChild(title); const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'multitext-save-choice-buttons'; const savePresetBtn = document.createElement('button'); savePresetBtn.textContent = 'Save as Preset'; savePresetBtn.className = 'multitext-save-choice-button'; savePresetBtn.onclick = () => { choiceDialog.remove(); this.showSavePresetDialog(textToSave, weightToSave, mainDialog); }; buttonsDiv.appendChild(savePresetBtn); const saveTemplateBtn = document.createElement('button'); saveTemplateBtn.textContent = 'Save as Template'; saveTemplateBtn.className = 'multitext-save-choice-button'; saveTemplateBtn.onclick = () => { choiceDialog.remove(); this.showSaveTemplateDialog(textToSave, weightToSave, mainDialog); }; buttonsDiv.appendChild(saveTemplateBtn); choiceDialog.appendChild(buttonsDiv); document.body.appendChild(choiceDialog); const rect = event.target.getBoundingClientRect(); const choiceRect = choiceDialog.getBoundingClientRect(); let top = rect.bottom + 5; let left = rect.left + (rect.width / 2) - (choiceRect.width / 2); if (left < 5) left = 5; if (left + choiceRect.width > window.innerWidth - 5) { left = window.innerWidth - choiceRect.width - 5; } if (top + choiceRect.height > window.innerHeight - 5) { top = rect.top - choiceRect.height - 5; } choiceDialog.style.left = `${left}px`; choiceDialog.style.top = `${top}px`; const closeListener = (e) => { if (!choiceDialog.contains(e.target) && e.target !== event.target) { choiceDialog.remove(); document.removeEventListener('mousedown', closeListener, true); } }; document.addEventListener('mousedown', closeListener, true); }, updateTemplateGrid(dialog) { const templateGrid = dialog.querySelector('.multitext-dialog-template-grid'); if (!templateGrid) return; templateGrid.innerHTML = ''; Object.entries(THEME.promptTemplates).forEach(([category, { icon, label, templates }]) => { if (!Array.isArray(templates)) { console.warn(`Templates for category '${category}' is not an array. Skipping.`); return; } templates.forEach(template => { const templateBtn = document.createElement('button'); templateBtn.classList.add('multitext-dialog-template-button'); const templateBtnHeader = document.createElement('div'); templateBtnHeader.classList.add('multitext-dialog-template-button-header'); templateBtnHeader.innerHTML = `${icon || '📝'} ${template.name}`; const templateInfo = document.createElement('div'); templateInfo.classList.add('multitext-dialog-template-button-info'); templateInfo.innerHTML = ` ${label || category} ${template.weight ? template.weight.toFixed(1) : '-'} `; const editIcon = document.createElement('div'); editIcon.classList.add('multitext-template-edit-icon'); editIcon.innerHTML = '⋮'; editIcon.title = 'Edit template'; editIcon.onclick = (e) => { e.stopPropagation(); this.showSaveTemplateDialog( template.prompt, template.weight, dialog, template.name, category ); }; templateBtn.appendChild(editIcon); templateBtn.appendChild(templateBtnHeader); templateBtn.appendChild(templateInfo); templateBtn.onclick = () => { const textarea = dialog.textarea; const weightInput = dialog.weightInput; if (textarea) { textarea.value = template.prompt; if (template.weight !== undefined && weightInput) { weightInput.value = template.weight; } textarea.dispatchEvent(new Event('input')); } }; templateGrid.appendChild(templateBtn); }); }); }, updateHistoryDropdown(dropdown) { if (!dropdown) return; dropdown.innerHTML = ''; const history = this.loadPromptHistory(); if (history.length === 0) { const emptyMessage = document.createElement('div'); emptyMessage.classList.add('multitext-dialog-history-empty'); emptyMessage.textContent = 'No history items'; dropdown.appendChild(emptyMessage); return; } history.forEach((item, index) => { const historyItem = document.createElement('div'); historyItem.classList.add('multitext-dialog-history-item'); const textSpan = document.createElement('span'); textSpan.classList.add('multitext-dialog-history-text'); textSpan.textContent = item.text; historyItem.appendChild(textSpan); const deleteBtn = document.createElement('button'); deleteBtn.classList.add('multitext-dialog-history-delete'); deleteBtn.innerHTML = '✕'; deleteBtn.onclick = (e) => { e.stopPropagation(); history.splice(index, 1); localStorage.setItem('multitext_history', JSON.stringify(history)); historyItem.remove(); if (history.length === 0) { const emptyMessage = document.createElement('div'); emptyMessage.classList.add('multitext-dialog-history-empty'); emptyMessage.textContent = 'No history items'; dropdown.appendChild(emptyMessage); } this.updateHistoryDropdown(dropdown); }; historyItem.appendChild(deleteBtn); historyItem.onclick = () => { const textarea = dropdown.closest('.multitext-dialog')?.querySelector('.multitext-dialog-textarea'); if (textarea) { textarea.value = item.text; textarea.dispatchEvent(new Event('input')); } dropdown.style.display = 'none'; }; historyItem.onmouseover = () => { deleteBtn.style.display = 'block'; }; historyItem.onmouseout = () => { deleteBtn.style.display = 'none'; }; dropdown.appendChild(historyItem); }); }, loadFonts() { if (document.getElementById('multitext-fonts')) return; const head = document.head; const fontElements = ` `; head.insertAdjacentHTML('beforeend', fontElements); }, _makeDialogDraggable(dialogElement, headerElement) { let isDragging = false; let xOffset = 0; let yOffset = 0; const removeListeners = () => { document.removeEventListener('mousemove', dragMove); document.removeEventListener('mouseup', dragEnd); if (headerElement) headerElement.style.cursor = 'move'; }; const dragStart = (e) => { if (headerElement && (e.target === headerElement || headerElement.contains(e.target)) && !e.target.closest('button')) { isDragging = true; dialogElement.style.transition = 'none'; headerElement.style.cursor = 'grabbing'; const rect = dialogElement.getBoundingClientRect(); xOffset = e.clientX - rect.left; yOffset = e.clientY - rect.top; e.preventDefault(); document.addEventListener('mousemove', dragMove); document.addEventListener('mouseup', dragEnd); } }; const dragMove = (e) => { if (isDragging) { e.preventDefault(); const x = e.clientX - xOffset; const y = e.clientY - yOffset; dialogElement.style.left = `${x}px`; dialogElement.style.top = `${y}px`; dialogElement.style.transform = 'none'; } }; const dragEnd = (e) => { if (isDragging) { isDragging = false; removeListeners(); } }; if (headerElement) { headerElement.style.cursor = 'move'; headerElement.addEventListener('mousedown', dragStart); } else { console.warn("Draggable dialog called without a valid header element."); } return () => { if (headerElement) { headerElement.removeEventListener('mousedown', dragStart); } removeListeners(); }; }, _showDeletePresetDialog(preset) { if (!preset) return; if (confirm(`Are you sure you want to delete the preset \"${preset.label}\"?`)) { let categoryFound = null; let presetIndex = -1; for (const category in THEME.promptPresets) { if (THEME.promptPresets[category]?.presets) { presetIndex = THEME.promptPresets[category].presets.findIndex(p => p.label === preset.label && p.value === preset.value); if (presetIndex > -1) { categoryFound = category; break; } } } if (categoryFound !== null && presetIndex > -1) { THEME.promptPresets[categoryFound].presets.splice(presetIndex, 1); localStorage.setItem('multitext_category_presets', JSON.stringify(THEME.promptPresets)); console.log(`Preset \"${preset.label}\" successfully deleted from ${categoryFound} category`); } else { console.warn("Could not find preset to delete in THEME structure:", preset); } } }, _showEditPresetDialog(preset, labelElementToUpdate = null) { if (!preset) return; const editDialog = DialogUtils.createDialog(THEME, { className: 'multitext-save-preset-dialog', }); editDialog.style.zIndex = '10002'; const editHeader = DialogUtils.createHeader(THEME, 'Edit Preset', [ DialogUtils.createButton(THEME, { innerHTML: '✕', className: 'multitext-dialog-button-close', onclick: () => editDialog.remove() }) ]); editDialog.appendChild(editHeader); const cleanupDragEdit = this._makeDialogDraggable(editDialog, editHeader); const nameInput = DialogUtils.createInput(THEME, { value: preset.label, placeholder: 'Preset name', className: 'multitext-save-preset-input' }); editDialog.appendChild(nameInput); const valueInput = DialogUtils.createTextarea(THEME, { value: preset.value, placeholder: 'Preset value', className: 'multitext-dialog-textarea' }); editDialog.appendChild(valueInput); const buttons = document.createElement('div'); buttons.classList.add('multitext-save-preset-buttons'); const cancelBtn = DialogUtils.createButton(THEME, { innerHTML: 'Cancel', className: 'multitext-save-preset-button multitext-save-preset-button-cancel', onclick: (e) => { e.stopPropagation(); editDialog.remove(); } }); const saveBtn = DialogUtils.createButton(THEME, { innerHTML: 'Save', className: 'multitext-save-preset-button multitext-save-preset-button-save', onclick: (e) => { e.stopPropagation(); const newName = nameInput.value.trim(); const newValue = valueInput.value.trim(); if (newName && newValue) { let originalCategory = null; for (const cat in THEME.promptPresets) { if (THEME.promptPresets[cat]?.presets?.some(p => p.label === preset.label && p.value === preset.value)) { originalCategory = cat; break; } } if (originalCategory) { const presetIndex = THEME.promptPresets[originalCategory].presets.findIndex(p => p.label === preset.label && p.value === preset.value); if (presetIndex > -1) { THEME.promptPresets[originalCategory].presets[presetIndex].label = newName; THEME.promptPresets[originalCategory].presets[presetIndex].value = newValue; preset.label = newName; preset.value = newValue; if (labelElementToUpdate) { labelElementToUpdate.textContent = newName; } this.saveToHistory(newValue); localStorage.setItem('multitext_category_presets', JSON.stringify(THEME.promptPresets)); } } else { console.warn("Could not find original category for preset:", preset); } editDialog.remove(); } } }); buttons.appendChild(cancelBtn); buttons.appendChild(saveBtn); editDialog.appendChild(buttons); editDialog.addEventListener('mousedown', (e) => { e.stopPropagation(); }); document.body.appendChild(editDialog); nameInput.focus(); nameInput.select(); requestAnimationFrame(() => { const container = document.querySelector('.graph-canvas-container') || document.body; const containerRect = container.getBoundingClientRect(); const dialogRect = editDialog.getBoundingClientRect(); let desiredLeft = (containerRect.width - dialogRect.width) / 2 + containerRect.left; let desiredTop = (containerRect.height - dialogRect.height) / 2 + containerRect.top; const clampedLeft = Math.max( containerRect.left + 10, Math.min(containerRect.right - dialogRect.width - 10, desiredLeft) ); const clampedTop = Math.max( containerRect.top + 10, Math.min(containerRect.bottom - dialogRect.height - 10, desiredTop) ); editDialog.style.left = clampedLeft + 'px'; editDialog.style.top = clampedTop + 'px'; editDialog.style.transform = 'none'; }); }, }; app.registerExtension({ name: "SKB.MultiText", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name !== "MultiTextNode") return; Object.assign(nodeType.prototype, MultiTextNode); } }); app.ui.settings.addSetting({ id: "SKB.ShowTooltips", name: "Show Multi-Text tooltips on hover?", type: "boolean", defaultValue: false, }); (function() { if (!app) { console.error("[MultiText Patch] ComfyUI app not found, cannot patch graphToPrompt."); return; } if (!app.originalMultiTextGraphToPrompt) { app.originalMultiTextGraphToPrompt = app.graphToPrompt; } app.graphToPrompt = async function () { const prompt = await app.originalMultiTextGraphToPrompt.apply(this, arguments); if (prompt && prompt.output) { for (const nodeId in prompt.output) { const nodeData = prompt.output[nodeId]; if (nodeData.class_type === "MultiTextNode") { const graphNode = this.graph.getNodeById(Number(nodeId)); if (graphNode) { if (!nodeData.inputs) { nodeData.inputs = {}; } nodeData.inputs.separator = graphNode.separatorValue ?? " "; nodeData.inputs.active = graphNode.isActive ?? true; } else { console.warn(`[MultiText graphToPrompt wrapper] Could not find graph node for node ID ${nodeId}.`); if (!nodeData.inputs) { nodeData.inputs = {}; } nodeData.inputs.separator = " "; nodeData.inputs.active = true; } } } } else { console.warn("[MultiText graphToPrompt wrapper] Prompt or prompt.output is undefined."); } return prompt; }; })();