Addressed compatibility issues from a recent ComfyUI update and introduced several new features and stability improvements across the entire node suite.
- **NodeDesign**: Implemented 'Place Adjacent' tools ('Place Right' & 'Place Below') for rapid, connection-aware alignment and removed outdated keyboard shortcuts.
- **PaintPro**: Added a new fullscreen editing mode and a refresh button for an improved workflow.
- **TitlePlus**: Refactored the animation engine to use transform offsets, fixing position corruption bugs and increasing stability.
- **Core**: Introduced a `dragHelper.js` utility to centralize and standardize mouse drag prevention logic.
- **Fixes & Polish**: Fixed a separator rendering bug in MultiText, patched a NullGraphError in LensFlare, and applied general compatibility fixes to other nodes.
2352 lines
118 KiB
JavaScript
2352 lines
118 KiB
JavaScript
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 = `<span style="opacity: 0.9">${icon}</span>`;
|
||
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 = `
|
||
<span>${label || category}</span>
|
||
<span>${template.weight ? template.weight.toFixed(1) : '-'}</span>
|
||
`;
|
||
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: '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',
|
||
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 = `
|
||
<span>${label || category}</span>
|
||
<span>${template.weight ? template.weight.toFixed(1) : '-'}</span>
|
||
`;
|
||
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 = `
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap">
|
||
<style id="multitext-fonts">
|
||
.multitext-node * {
|
||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||
Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||
}
|
||
</style>
|
||
`;
|
||
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;
|
||
};
|
||
})();
|