476 lines
19 KiB
JavaScript
476 lines
19 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
app.registerExtension({
|
|
name: "RaykoStudio.ImagePrompt",
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name !== "RS_ImagePrompt") return;
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
|
const node = this;
|
|
|
|
node.data = {
|
|
selected_image: "",
|
|
prompt: ""
|
|
};
|
|
|
|
const MIN_WIDTH = 360;
|
|
const MIN_HEIGHT = 460;
|
|
const PREVIEW_FIXED_HEIGHT = 150;
|
|
|
|
node.MIN_WIDTH = MIN_WIDTH;
|
|
node.MIN_HEIGHT = MIN_HEIGHT;
|
|
node.setSize([MIN_WIDTH, MIN_HEIGHT]);
|
|
|
|
const w_image = node.widgets?.find(w => w.name === "image");
|
|
const w_prompt = node.widgets?.find(w => w.name === "prompt_preview");
|
|
|
|
const syncWidget = (widget, dataKey) => {
|
|
if (widget) {
|
|
widget.value = node.data[dataKey];
|
|
widget.serializeValue = () => node.data[dataKey];
|
|
}
|
|
};
|
|
|
|
if (node.widgets) {
|
|
node.widgets.forEach(w => {
|
|
if (w.name === "image" && w.value) node.data.selected_image = w.value;
|
|
if (w.name === "prompt_preview" && w.value) node.data.prompt = w.value;
|
|
|
|
w.hidden = true;
|
|
w.type = "hidden";
|
|
w.computeSize = () => [0, 0];
|
|
w.computedHeight = 0;
|
|
w.draw = function () {};
|
|
|
|
if (w.element && w.element.parentNode) {
|
|
w.element.parentNode.removeChild(w.element);
|
|
}
|
|
});
|
|
}
|
|
|
|
syncWidget(w_image, "selected_image");
|
|
syncWidget(w_prompt, "prompt");
|
|
|
|
node.syncData = function () {
|
|
if (node.graph) {
|
|
node.graph.setDirtyCanvas(true, true);
|
|
node.graph.changeTracker?.dispatchEvent(new Event("change"));
|
|
}
|
|
};
|
|
|
|
node.imageLoaded = false;
|
|
node.imageLoading = false;
|
|
|
|
let promptRequestId = 0;
|
|
|
|
const mainContainer = document.createElement("div");
|
|
mainContainer.style.cssText = `
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding: 0 !important;
|
|
margin: 0 !important;
|
|
box-sizing: border-box;
|
|
pointer-events: none;
|
|
position: relative;
|
|
`;
|
|
|
|
const toast = document.createElement("div");
|
|
toast.style.cssText = "position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); padding:6px 14px; border-radius:6px; color:#fff; font:bold 14px Arial; z-index:10; pointer-events:none; display:none; white-space:nowrap;";
|
|
mainContainer.appendChild(toast);
|
|
|
|
let toastTimer = null;
|
|
node.showToast = function (text, type) {
|
|
let color, borderColor, borderWidth;
|
|
if (type === "success") {
|
|
color = "rgba(34, 197, 94, 0.97)";
|
|
borderColor = "rgba(255, 255, 255, 0.95)";
|
|
borderWidth = 2;
|
|
} else if (type === "warning") {
|
|
color = "rgba(249, 115, 22, 0.97)";
|
|
borderColor = "rgba(220, 38, 38, 1)";
|
|
borderWidth = 3;
|
|
} else {
|
|
color = "rgba(239, 68, 68, 0.97)";
|
|
borderColor = "rgba(255, 255, 255, 0.95)";
|
|
borderWidth = 2;
|
|
}
|
|
toast.textContent = text;
|
|
toast.style.background = color;
|
|
toast.style.border = `${borderWidth}px solid ${borderColor}`;
|
|
toast.style.display = "block";
|
|
if (toastTimer) clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(() => {
|
|
toast.style.display = "none";
|
|
}, 2000);
|
|
};
|
|
|
|
const onDocumentDragOver = (e) => {
|
|
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
const onDocumentDrop = (e) => {
|
|
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
document.addEventListener('dragover', onDocumentDragOver);
|
|
document.addEventListener('drop', onDocumentDrop);
|
|
node._docDragOver = onDocumentDragOver;
|
|
node._docDrop = onDocumentDrop;
|
|
|
|
const textContainer = document.createElement("div");
|
|
textContainer.style.cssText = "width:100%; pointer-events:auto;";
|
|
|
|
const customTextArea = document.createElement("textarea");
|
|
customTextArea.readOnly = true;
|
|
customTextArea.value = node.data.prompt;
|
|
customTextArea.placeholder = "Prompt will appear here after loading an image...";
|
|
customTextArea.style.cssText = "width:100%; min-height:50px; padding:6px; background:#1e1e1e; color:#eee; border:1px solid #444; border-radius:4px; resize:none; font-family:monospace; font-size:12px; box-sizing:border-box; pointer-events:auto; cursor:text;";
|
|
textContainer.appendChild(customTextArea);
|
|
|
|
const buttonsContainer = document.createElement("div");
|
|
buttonsContainer.style.cssText = "display:flex; gap:6px; width:100%; pointer-events:auto;";
|
|
|
|
const btnUpload = document.createElement("button");
|
|
btnUpload.textContent = "📂 UPLOAD IMAGE";
|
|
btnUpload.style.cssText = "flex:3; height:28px; background:#2a2a2a; color:#4CAF50; border:2px solid #4CAF50; border-radius:4px; cursor:pointer; font-weight:normal; font-size:12px; transition:all 0.2s; pointer-events:auto;";
|
|
btnUpload.onmouseover = () => { btnUpload.style.backgroundColor = "#3a3a3a"; };
|
|
btnUpload.onmouseout = () => { btnUpload.style.backgroundColor = "#2a2a2a"; };
|
|
btnUpload.onclick = () => node.triggerFileUpload();
|
|
|
|
const btnCopy = document.createElement("button");
|
|
btnCopy.textContent = "📝 COPY";
|
|
btnCopy.title = "Copy prompt to clipboard";
|
|
btnCopy.style.cssText = "flex:1; height:28px; background:#2a2a2a; color:#64B5F6; border:2px solid #2196F3; border-radius:4px; cursor:pointer; font-weight:normal; font-size:12px; transition:all 0.2s; pointer-events:auto;";
|
|
btnCopy.onmouseover = () => { if (customTextArea.value) btnCopy.style.backgroundColor = "#3a3a3a"; };
|
|
btnCopy.onmouseout = () => { btnCopy.style.backgroundColor = "#2a2a2a"; };
|
|
btnCopy.onclick = () => copyPrompt();
|
|
|
|
buttonsContainer.appendChild(btnUpload);
|
|
buttonsContainer.appendChild(btnCopy);
|
|
|
|
const previewWrap = document.createElement("div");
|
|
previewWrap.style.cssText = `
|
|
width: 100%;
|
|
height: ${PREVIEW_FIXED_HEIGHT}px;
|
|
background: #353535;
|
|
border: 1px dashed #555;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
position: relative;
|
|
pointer-events: auto;
|
|
cursor: pointer;
|
|
box-sizing: border-box;
|
|
`;
|
|
|
|
const placeholder = document.createElement("div");
|
|
placeholder.textContent = "📁 Drop Image Here or Click to Upload";
|
|
placeholder.style.cssText = "color:#888; font-size:14px; font-family:Arial,sans-serif; text-align:center; pointer-events:none; user-select:none;";
|
|
previewWrap.appendChild(placeholder);
|
|
|
|
const previewImg = document.createElement("img");
|
|
previewImg.style.cssText = "display:none; width:100%; height:100%; object-fit:contain; pointer-events:none; user-select:none;";
|
|
previewImg.alt = "Image preview";
|
|
previewWrap.appendChild(previewImg);
|
|
|
|
mainContainer.appendChild(textContainer);
|
|
mainContainer.appendChild(buttonsContainer);
|
|
mainContainer.appendChild(previewWrap);
|
|
|
|
const copyPrompt = async () => {
|
|
const text = customTextArea.value;
|
|
if (!text) return;
|
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
node.showToast("Copy done!", "success");
|
|
return;
|
|
} catch (e) { }
|
|
}
|
|
|
|
try {
|
|
customTextArea.focus();
|
|
customTextArea.select();
|
|
const ok = document.execCommand("copy");
|
|
customTextArea.setSelectionRange(0, 0);
|
|
customTextArea.blur();
|
|
if (ok) node.showToast("Copy done!", "success");
|
|
else node.showToast("Copy error!", "error");
|
|
} catch (e) {
|
|
node.showToast("Copy error!", "error");
|
|
}
|
|
};
|
|
|
|
const updateCopyBtnState = () => {
|
|
if (!btnCopy) return;
|
|
const has = !!customTextArea.value;
|
|
btnCopy.style.opacity = has ? "1" : "0.4";
|
|
btnCopy.style.cursor = has ? "pointer" : "not-allowed";
|
|
};
|
|
|
|
const fetchPrompt = async (imagePath, notifyNotFound) => {
|
|
if (!imagePath) return;
|
|
|
|
const requestId = ++promptRequestId;
|
|
|
|
node.data.prompt = "";
|
|
customTextArea.value = "";
|
|
customTextArea.placeholder = "⏳ Loading prompt...";
|
|
syncWidget(w_prompt, "prompt");
|
|
updateCopyBtnState();
|
|
node.syncData();
|
|
|
|
try {
|
|
const response = await fetch(`/rayko/get_prompt?filename=${encodeURIComponent(imagePath)}`);
|
|
|
|
if (requestId !== promptRequestId) return;
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const result = await response.json();
|
|
|
|
if (requestId !== promptRequestId) return;
|
|
|
|
node.data.prompt = result.prompt || "";
|
|
customTextArea.value = node.data.prompt;
|
|
customTextArea.placeholder = node.data.prompt
|
|
? ""
|
|
: "⚠ Prompt not found in this image";
|
|
syncWidget(w_prompt, "prompt");
|
|
updateCopyBtnState();
|
|
node.syncData();
|
|
|
|
if (!node.data.prompt && notifyNotFound) {
|
|
node.showToast("⚠ Prompt not found", "warning");
|
|
}
|
|
} catch (err) {
|
|
if (requestId !== promptRequestId) return;
|
|
customTextArea.placeholder = "⚠ Prompt not found in this image";
|
|
}
|
|
};
|
|
|
|
const buildViewUrl = (imagePath) => {
|
|
let filename = imagePath;
|
|
let subfolder = "";
|
|
if (imagePath.includes("/")) {
|
|
const parts = imagePath.split("/");
|
|
subfolder = parts[0];
|
|
filename = parts.slice(1).join("/");
|
|
}
|
|
let url = `/view?filename=${encodeURIComponent(filename)}&type=input`;
|
|
if (subfolder) url += `&subfolder=${encodeURIComponent(subfolder)}`;
|
|
return url + `&t=${Date.now()}`;
|
|
};
|
|
|
|
node.loadImage = function (imagePath, notifyNotFound) {
|
|
if (!imagePath || node.imageLoading) return;
|
|
|
|
node.imageLoading = true;
|
|
node.data.selected_image = imagePath;
|
|
syncWidget(w_image, "selected_image");
|
|
|
|
previewImg.onload = () => {
|
|
node.imageLoading = false;
|
|
node.imageLoaded = true;
|
|
previewImg.style.display = "block";
|
|
placeholder.style.display = "none";
|
|
node.syncData();
|
|
fetchPrompt(imagePath, notifyNotFound);
|
|
};
|
|
previewImg.onerror = () => {
|
|
node.imageLoading = false;
|
|
node.imageLoaded = false;
|
|
previewImg.style.display = "none";
|
|
placeholder.style.display = "block";
|
|
node.syncData();
|
|
};
|
|
previewImg.src = buildViewUrl(imagePath);
|
|
};
|
|
|
|
const uploadFileAndLoad = async (file) => {
|
|
if (!file) return false;
|
|
|
|
const SUPPORTED_EXT = /\.(png|jpe?g|webp)$/i;
|
|
const SUPPORTED_MIME = ['image/png', 'image/jpeg', 'image/webp'];
|
|
const isSupported = SUPPORTED_EXT.test(file.name) || SUPPORTED_MIME.includes(file.type);
|
|
|
|
if (!isSupported) {
|
|
node.showToast("⚠ Only PNG/JPG/WebP supported", "warning");
|
|
return false;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
formData.append('subfolder', '');
|
|
formData.append('type', 'input');
|
|
|
|
try {
|
|
const response = await fetch('/upload/image', { method: 'POST', body: formData });
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
const imageName = result.name || result.filename;
|
|
const subfolder = result.subfolder || '';
|
|
const finalName = subfolder ? `${subfolder}/${imageName}` : imageName;
|
|
node.loadImage(finalName, true);
|
|
return true;
|
|
}
|
|
} catch (err) {
|
|
console.error("[RS Image-Prompt] Upload error:", err);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
node.triggerFileUpload = function () {
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.accept = 'image/png,image/jpeg,image/webp';
|
|
fileInput.onchange = async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
await uploadFileAndLoad(file);
|
|
fileInput.remove();
|
|
};
|
|
fileInput.click();
|
|
};
|
|
|
|
previewWrap.addEventListener('dragover', (e) => {
|
|
if (!e.dataTransfer || !e.dataTransfer.types.includes('Files')) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
previewWrap.style.borderColor = '#4CAF50';
|
|
previewWrap.style.backgroundColor = '#2a2a2a';
|
|
});
|
|
|
|
previewWrap.addEventListener('dragleave', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
previewWrap.style.borderColor = '#555';
|
|
previewWrap.style.backgroundColor = '#353535';
|
|
});
|
|
|
|
previewWrap.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
previewWrap.style.borderColor = '#555';
|
|
previewWrap.style.backgroundColor = '#353535';
|
|
|
|
const file = e.dataTransfer.files[0];
|
|
if (file) {
|
|
uploadFileAndLoad(file);
|
|
}
|
|
});
|
|
|
|
previewWrap.addEventListener('click', () => {
|
|
node.triggerFileUpload();
|
|
});
|
|
|
|
node.addDOMWidget("custom_widgets", "customtext", mainContainer);
|
|
node.customTextArea = customTextArea;
|
|
|
|
const updateUIFromData = () => {
|
|
customTextArea.value = node.data.prompt || "";
|
|
updateCopyBtnState();
|
|
};
|
|
|
|
node.onSerialize = function (o) {
|
|
syncWidget(w_image, "selected_image");
|
|
syncWidget(w_prompt, "prompt");
|
|
o.data = node.data;
|
|
};
|
|
|
|
node.onConfigure = function (o) {
|
|
if (o && o.data) {
|
|
node.data = { ...node.data, ...o.data };
|
|
}
|
|
|
|
node.imageLoading = false;
|
|
node.imageLoaded = false;
|
|
|
|
if (node.data.selected_image) {
|
|
node.loadImage(node.data.selected_image);
|
|
}
|
|
|
|
updateUIFromData();
|
|
|
|
syncWidget(w_image, "selected_image");
|
|
syncWidget(w_prompt, "prompt");
|
|
|
|
requestAnimationFrame(() => {
|
|
if (node.size) {
|
|
node.onResize([...node.size]);
|
|
}
|
|
});
|
|
};
|
|
|
|
node.visibilityHandler = function () {
|
|
if (!document.hidden && node.graph) {
|
|
node.graph.setDirtyCanvas(true, true);
|
|
node.syncData();
|
|
|
|
if (node.size) {
|
|
node.onResize([...node.size]);
|
|
}
|
|
}
|
|
};
|
|
document.addEventListener("visibilitychange", node.visibilityHandler);
|
|
|
|
const originalOnResize = node.onResize;
|
|
node.onResize = function (size) {
|
|
if (size[0] < MIN_WIDTH) size[0] = MIN_WIDTH;
|
|
if (size[1] < MIN_HEIGHT) size[1] = MIN_HEIGHT;
|
|
|
|
const titleBarHeight = 30;
|
|
const topPadding = 18;
|
|
const gap = 8;
|
|
const bottomPadding = 0;
|
|
const previewSpacing = 20;
|
|
|
|
const fixedElements = titleBarHeight + topPadding +
|
|
28 + gap +
|
|
previewSpacing +
|
|
PREVIEW_FIXED_HEIGHT +
|
|
bottomPadding;
|
|
|
|
const textareaHeight = Math.max(50, size[1] - fixedElements);
|
|
customTextArea.style.height = textareaHeight + 'px';
|
|
|
|
if (originalOnResize) originalOnResize.apply(this, arguments);
|
|
node.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
const originalOnRemoved = node.onRemoved;
|
|
node.onRemoved = function () {
|
|
document.removeEventListener("visibilitychange", node.visibilityHandler);
|
|
if (node._docDragOver) {
|
|
document.removeEventListener('dragover', node._docDragOver);
|
|
}
|
|
if (node._docDrop) {
|
|
document.removeEventListener('drop', node._docDrop);
|
|
}
|
|
if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
|
|
};
|
|
|
|
if (node.data.selected_image) {
|
|
node.loadImage(node.data.selected_image);
|
|
}
|
|
|
|
updateCopyBtnState();
|
|
|
|
requestAnimationFrame(() => {
|
|
node.onResize([...node.size]);
|
|
});
|
|
|
|
return result;
|
|
};
|
|
},
|
|
setup() {}
|
|
}); |