The Node Assistant becomes "Current node" (当前节点): one page instead of the "actions" / "parameter sets" tabs, model actions on top, Parameters below. - node_parameter_sets.js: the node type's saved values from the material library and from recipes' parameter sets as one list; the same values saved in both are one entry naming both sources. For the selected node, exactly what an entry would change; seeds (and their after-run control), model files and choices this computer lacks are kept. - ui_node_parameters.js: the list, "apply these N" (one undo in the receipt) and "save these values". - api/node_material.py: POST /anomalous/save_node_material keeps one node's values as a node_parameter_selection material, deduplicated by type and values; the library lists it under parameters. - Removed: ui_node_presets.js with its hover popovers, the beta notice and their styles and strings. The panel and help texts use the new name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
466 lines
28 KiB
JavaScript
466 lines
28 KiB
JavaScript
/** The current-node panel (Node Assistant): the selected node's model actions (change it,
|
|
* insert a LoRA), model cards with their history, and its parameters (ui_node_parameters.js). */
|
|
|
|
import { app } from "../../../scripts/app.js";
|
|
import { translate } from "./locales.js";
|
|
import { analyzeModelChainInsertion, getModelChainInsertionCapabilities } from "./graph_splice.js";
|
|
import { formatModelTypeLabel, inferPickerModelType } from "./model_picker.js";
|
|
import { findModelComboWidget, getNativeWidgetValues } from "./ui_node_model_picker.js";
|
|
import { renderNodeParameters } from "./ui_node_parameters.js";
|
|
|
|
const t = (key, params) => translate(key, params);
|
|
|
|
export function initAssistantPanel() {
|
|
this.assistantPanelInitialized = true;
|
|
this.assistantPanel.innerHTML = '';
|
|
this.assistantPanel.style.padding = '0';
|
|
this.assistantPanel.style.overflow = 'hidden';
|
|
|
|
if (!this._assistantPanelHooked) {
|
|
this._assistantPanelHooked = true;
|
|
const self = this;
|
|
const originalOnSelected = app.canvas.onNodeSelected;
|
|
app.canvas.onNodeSelected = function (node) {
|
|
if (originalOnSelected) originalOnSelected.apply(this, arguments);
|
|
if (self.assistantPanel && self.assistantPanel.style.display !== 'none') {
|
|
self.diagnoseNode(node);
|
|
}
|
|
};
|
|
const originalOnDeselected = app.canvas.onNodeDeselected;
|
|
app.canvas.onNodeDeselected = function (node) {
|
|
if (originalOnDeselected) originalOnDeselected.apply(this, arguments);
|
|
if (self.assistantPanel && self.assistantPanel.style.display !== 'none') {
|
|
const stillSelected = Object.values(app.canvas.selected_nodes || {});
|
|
if (stillSelected.length > 0) self.diagnoseNode(stillSelected[0]);
|
|
else self.diagnoseNode(null);
|
|
}
|
|
};
|
|
}
|
|
|
|
const placeholder = document.createElement('div');
|
|
placeholder.id = 'anomalous-assistant-placeholder';
|
|
placeholder.style.cssText = 'display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;color:#666;font-size:15px;gap:12px;padding:40px;text-align:center;';
|
|
placeholder.innerHTML = `<div style="font-size:48px;">🤖</div><div>${t('assistantSelectNode')}</div>`;
|
|
this.assistantPanel.appendChild(placeholder);
|
|
|
|
const nodeContent = document.createElement('div');
|
|
nodeContent.id = 'anomalous-assistant-node-content';
|
|
nodeContent.style.cssText = 'display:none;flex-direction:column;flex:1;min-height:0;overflow:hidden;';
|
|
this.assistantPanel.appendChild(nodeContent);
|
|
}
|
|
|
|
export function renderAssistantModelCard(node, w, container) {
|
|
const val = w.value;
|
|
const filename = val.split(/[\\/]/).pop();
|
|
const pickerType = inferPickerModelType(node, w);
|
|
|
|
const wrapper = document.createElement('div');
|
|
wrapper.style.cssText = 'margin:12px 16px 16px;padding:10px;border:1px solid rgba(255,255,255,0.075);border-radius:14px;background:linear-gradient(160deg,rgba(31,33,42,0.96),rgba(20,21,27,0.96));display:flex;flex-direction:column;gap:12px;box-shadow:0 16px 35px rgba(0,0,0,0.2);';
|
|
|
|
// Preview image
|
|
const previewBox = document.createElement('div');
|
|
previewBox.style.cssText = 'width:100%;aspect-ratio:1.65;max-height:260px;background:radial-gradient(circle at 50% 20%,#252a3b,#0b0c10 70%);border-radius:10px;overflow:hidden;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,0.05);';
|
|
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${t('doctorLoadingPreview')}</span>`;
|
|
wrapper.appendChild(previewBox);
|
|
|
|
// Name and path
|
|
const identityRow = document.createElement('div');
|
|
identityRow.style.cssText = 'display:flex;align-items:flex-start;gap:10px;padding:0 3px;';
|
|
const identityCopy = document.createElement('div');
|
|
identityCopy.style.cssText = 'display:flex;flex-direction:column;gap:4px;min-width:0;flex:1;';
|
|
const nameEl = document.createElement('div');
|
|
nameEl.style.cssText = 'color:#fff;font-weight:750;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
|
|
nameEl.title = filename;
|
|
nameEl.innerText = filename;
|
|
const pathEl = document.createElement('div');
|
|
pathEl.style.cssText = 'color:#646b7a;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
|
|
pathEl.title = val;
|
|
pathEl.innerText = val;
|
|
const modelTypeBadge = document.createElement('span');
|
|
modelTypeBadge.textContent = pickerType.label;
|
|
modelTypeBadge.style.cssText = 'padding:5px 8px;border-radius:999px;background:rgba(138,180,248,0.1);border:1px solid rgba(138,180,248,0.22);color:#a9c7ff;font-size:9px;font-weight:750;white-space:nowrap;';
|
|
identityCopy.append(nameEl, pathEl);
|
|
identityRow.append(identityCopy, modelTypeBadge);
|
|
wrapper.appendChild(identityRow);
|
|
|
|
// Metadata zone (populated async)
|
|
const metaZone = document.createElement('div');
|
|
metaZone.style.cssText = 'display:flex;flex-direction:column;gap:10px;';
|
|
wrapper.appendChild(metaZone);
|
|
|
|
// Secondary action; model replacement lives in the prominent node toolbar.
|
|
const actionRow = document.createElement('div');
|
|
actionRow.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;';
|
|
const profileBtn = document.createElement('button');
|
|
profileBtn.textContent = t('doctorProfile');
|
|
profileBtn.style.cssText = 'flex:1;padding:10px 12px;background:rgba(138,180,248,0.09);color:#a9c7ff;border:1px solid rgba(138,180,248,0.22);border-radius:9px;cursor:pointer;font-size:12px;font-weight:700;transition:filter 0.2s;';
|
|
profileBtn.onmouseover = () => profileBtn.style.filter = 'brightness(1.2)';
|
|
profileBtn.onmouseout = () => profileBtn.style.filter = 'brightness(1)';
|
|
|
|
actionRow.appendChild(profileBtn);
|
|
wrapper.appendChild(actionRow);
|
|
container.appendChild(wrapper);
|
|
|
|
// Async: load preview + metadata
|
|
fetch(`/anomalous/find_model?search=${encodeURIComponent(val.replace(/\\/g, '/'))}`)
|
|
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
|
.then(d => {
|
|
// Preview
|
|
if (d.status === 'success' && d.model && d.model.preview_url) {
|
|
previewBox.innerHTML = '';
|
|
const pu = d.model.preview_url;
|
|
const isVid = /\.(mp4|webm)(?:$|\?|&|#)/i.test(pu);
|
|
if (isVid) {
|
|
const vid = document.createElement('video');
|
|
vid.src = pu; vid.muted = true; vid.loop = true; vid.autoplay = true; vid.playsInline = true;
|
|
vid.style.cssText = 'width:100%;height:100%;object-fit:contain;';
|
|
previewBox.appendChild(vid);
|
|
} else {
|
|
const img = document.createElement('img');
|
|
img.src = pu;
|
|
img.style.cssText = 'width:100%;height:100%;object-fit:contain;';
|
|
previewBox.appendChild(img);
|
|
}
|
|
} else {
|
|
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${t('doctorNoPreview')}</span>`;
|
|
}
|
|
|
|
// Profile button links to detail
|
|
if (d.status === 'success' && d.model) {
|
|
profileBtn.onclick = () => {
|
|
this.assistantPanel.style.display = 'none';
|
|
this.currentType = d.type;
|
|
this.currentPathIdx = d.path_idx;
|
|
this.currentSubfolder = d.subfolder;
|
|
this.historyStack = this.historyStack || [];
|
|
this.historyStack.push({ type: 'assistant' });
|
|
this.showDetail(d.model);
|
|
if (this.foldersData) this.renderSidebar();
|
|
};
|
|
}
|
|
|
|
// Metadata
|
|
if (d.status === 'success' && d.model && d.model.metadata) {
|
|
const meta = d.model.metadata;
|
|
|
|
// Type + base model badges
|
|
if (d.type) modelTypeBadge.textContent = formatModelTypeLabel(d.type, pickerType.label);
|
|
if (meta.baseModel) {
|
|
const badgeRow = document.createElement('div');
|
|
badgeRow.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;';
|
|
const b = document.createElement('span');
|
|
b.style.cssText = 'background:rgba(0,255,204,0.08);border:1px solid rgba(0,255,204,0.15);color:#77e6cf;padding:4px 8px;border-radius:999px;font-size:10px;';
|
|
b.textContent = `${t('doctorBase')} · ${meta.baseModel}`;
|
|
badgeRow.appendChild(b);
|
|
metaZone.appendChild(badgeRow);
|
|
}
|
|
|
|
// Trigger words
|
|
const triggers = meta.trainedWords || meta.trigger_words || meta.trained_words;
|
|
if (triggers && triggers.length > 0) {
|
|
const trigSection = document.createElement('div');
|
|
trigSection.style.cssText = 'background:rgba(255,255,255,0.04);border-radius:6px;padding:10px 12px;';
|
|
const trigTitle = document.createElement('div');
|
|
trigTitle.style.cssText = 'color:#aaa;font-size:11px;margin-bottom:8px;font-weight:bold;text-transform:uppercase;letter-spacing:0.5px;';
|
|
trigTitle.textContent = t('doctorTriggerWords');
|
|
const tagList = document.createElement('div');
|
|
tagList.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px;';
|
|
const words = Array.isArray(triggers) ? triggers : [triggers];
|
|
words.forEach(word => {
|
|
const tag = document.createElement('span');
|
|
tag.style.cssText = 'background:rgba(255,193,7,0.12);color:#ffc107;padding:3px 8px;border-radius:4px;font-size:12px;cursor:pointer;';
|
|
tag.innerText = word;
|
|
tag.title = t('doctorClickCopy');
|
|
tag.onclick = () => {
|
|
navigator.clipboard.writeText(word).then(() => { const o = tag.innerText; tag.innerText = '✅'; setTimeout(() => tag.innerText = o, 1000); });
|
|
};
|
|
tagList.appendChild(tag);
|
|
});
|
|
const copyAll = document.createElement('button');
|
|
copyAll.style.cssText = 'background:transparent;border:1px solid #444;color:#888;border-radius:4px;padding:3px 8px;font-size:11px;cursor:pointer;margin-top:4px;';
|
|
copyAll.textContent = t('doctorCopyAll');
|
|
copyAll.onclick = () => {
|
|
navigator.clipboard.writeText(words.join(', ')).then(() => { copyAll.textContent = '✅'; setTimeout(() => copyAll.textContent = t('doctorCopyAll'), 1500); });
|
|
};
|
|
trigSection.appendChild(trigTitle);
|
|
trigSection.appendChild(tagList);
|
|
trigSection.appendChild(copyAll);
|
|
metaZone.appendChild(trigSection);
|
|
}
|
|
|
|
// Custom notes (parchment)
|
|
const textNotes = meta.custom_notes || meta.notes;
|
|
if (textNotes) {
|
|
const notesCard = document.createElement('div');
|
|
notesCard.style.cssText = 'background:linear-gradient(135deg,#262522 0%,#202124 100%);border:1px solid #3c4043;border-left:4px solid #a38d53;border-radius:4px 8px 8px 4px;padding:12px 14px;';
|
|
const notesTitle = document.createElement('div');
|
|
notesTitle.style.cssText = 'color:#a38d53;font-size:11px;font-weight:bold;margin-bottom:6px;';
|
|
notesTitle.textContent = t('doctorNotes');
|
|
const notesText = document.createElement('div');
|
|
notesText.style.cssText = 'color:#d4c4a0;font-size:13px;line-height:1.6;white-space:pre-wrap;';
|
|
notesText.innerText = textNotes;
|
|
notesCard.appendChild(notesTitle);
|
|
notesCard.appendChild(notesText);
|
|
metaZone.appendChild(notesCard);
|
|
}
|
|
}
|
|
|
|
// History gallery — always load if we can resolve the filename
|
|
const resolvedFilename = (d.status === 'success' && d.model) ? (d.model.filename || filename) : filename;
|
|
this._loadAssistantHistory(resolvedFilename, metaZone, d.status === 'success' ? d.model : null);
|
|
}).catch(() => {
|
|
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${t('assistantPreviewFailed')}</span>`;
|
|
|
|
// Still try to load history gallery by filename
|
|
this._loadAssistantHistory(filename, metaZone, null);
|
|
});
|
|
}
|
|
|
|
export function _loadAssistantHistory(filename, container, model) {
|
|
fetch('/anomalous/model_images?model_name=' + encodeURIComponent(filename) + '&t=' + Date.now())
|
|
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
|
.then(data => {
|
|
const images = data.images || [];
|
|
if (images.length === 0) return;
|
|
|
|
const section = document.createElement('div');
|
|
section.style.cssText = 'display:flex;flex-direction:column;gap:8px;';
|
|
|
|
// Section header with count + full gallery button
|
|
const sectionHeader = document.createElement('div');
|
|
sectionHeader.style.cssText = 'display:flex;align-items:center;justify-content:space-between;';
|
|
const sectionTitle = document.createElement('div');
|
|
sectionTitle.style.cssText = 'color:#aaa;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:0.5px;';
|
|
sectionTitle.textContent = t('assistantHistoryTitle', { count: images.length });
|
|
sectionHeader.appendChild(sectionTitle);
|
|
|
|
// "View all" button if model is available
|
|
if (model) {
|
|
const viewAllBtn = document.createElement('button');
|
|
viewAllBtn.textContent = t('assistantViewAll');
|
|
viewAllBtn.style.cssText = 'background:transparent;border:1px solid rgba(255,255,255,0.2);color:#e5e7eb;font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer;transition:all 0.2s;';
|
|
viewAllBtn.onmouseover = () => { viewAllBtn.style.background = 'rgba(255,255,255,0.1)'; };
|
|
viewAllBtn.onmouseout = () => { viewAllBtn.style.background = 'transparent'; };
|
|
viewAllBtn.onclick = () => this.showGeneratedGallery(model);
|
|
sectionHeader.appendChild(viewAllBtn);
|
|
}
|
|
section.appendChild(sectionHeader);
|
|
|
|
const grid = document.createElement('div');
|
|
grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:6px;';
|
|
images.slice(0, 16).forEach(img => {
|
|
const card = document.createElement('div');
|
|
card.style.cssText = 'border-radius:6px;overflow:hidden;aspect-ratio:1;background:#111;cursor:pointer;transition:transform 0.15s,box-shadow 0.15s;';
|
|
card.onmouseover = () => {
|
|
card.style.transform = 'scale(1.05)'; card.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)';
|
|
const v = card.querySelector('video'); if (v) v.play().catch(()=>{});
|
|
};
|
|
card.onmouseout = () => {
|
|
card.style.transform = 'scale(1)'; card.style.boxShadow = 'none';
|
|
const v = card.querySelector('video'); if (v) v.pause();
|
|
};
|
|
const imgUrl = img.url || img;
|
|
const isVid = /\.(mp4|webm)(?:$|\?|&|#)/i.test(imgUrl);
|
|
if (isVid) {
|
|
const vidEl = document.createElement('video');
|
|
vidEl.src = imgUrl; vidEl.muted = true; vidEl.loop = true; vidEl.autoplay = false; vidEl.playsInline = true; vidEl.preload = 'metadata';
|
|
vidEl.style.cssText = 'width:100%;height:100%;object-fit:cover;';
|
|
card.appendChild(vidEl);
|
|
} else {
|
|
const imgEl = document.createElement('img');
|
|
imgEl.src = imgUrl;
|
|
imgEl.style.cssText = 'width:100%;height:100%;object-fit:cover;';
|
|
imgEl.loading = 'lazy';
|
|
card.appendChild(imgEl);
|
|
}
|
|
if (img.workflow) {
|
|
card.title = t('assistantRestoreWorkflow');
|
|
card.onclick = () => {
|
|
try {
|
|
const wf = typeof img.workflow === 'string' ? JSON.parse(img.workflow) : img.workflow;
|
|
if (app && app.loadGraphData) app.loadGraphData(wf);
|
|
} catch (e) { }
|
|
};
|
|
} else if (model) {
|
|
card.title = t('assistantViewGallery');
|
|
card.onclick = () => this.showGeneratedGallery(model);
|
|
}
|
|
grid.appendChild(card);
|
|
});
|
|
section.appendChild(grid);
|
|
container.appendChild(section);
|
|
}).catch(() => { });
|
|
}
|
|
|
|
function getInsertionCapabilityMessage(capability) {
|
|
const messages = {
|
|
missing_graph_or_node: 'doctorGraphUnavailable',
|
|
missing_chain_inputs: 'doctorMissingChainInputs',
|
|
unconnected_chain_inputs: 'doctorUnconnectedChainInputs',
|
|
missing_chain_outputs: 'doctorMissingChainOutputs',
|
|
ambiguous_downstream_branches: 'doctorAmbiguousBranches',
|
|
invalid_downstream_link: 'doctorInvalidDownstream',
|
|
};
|
|
return t(messages[capability?.code] || 'doctorUnsupportedInsertion');
|
|
}
|
|
|
|
export function openLoraInsertionPicker(anchorNode, direction) {
|
|
const analysis = analyzeModelChainInsertion(app.graph, anchorNode, direction);
|
|
if (!analysis.supported) {
|
|
alert(getInsertionCapabilityMessage(analysis));
|
|
return;
|
|
}
|
|
|
|
const insertedNode = typeof LiteGraph !== 'undefined' ? LiteGraph.createNode('LoraLoader') : null;
|
|
if (!insertedNode) {
|
|
alert(t('doctorCreateLoraFailed'));
|
|
return;
|
|
}
|
|
|
|
const modelWidget = findModelComboWidget(insertedNode);
|
|
if (!modelWidget || getNativeWidgetValues(insertedNode, modelWidget).length === 0) {
|
|
alert(t('doctorLoraNotReady'));
|
|
return;
|
|
}
|
|
|
|
this._openGalleryReplacer(insertedNode, modelWidget, {
|
|
mode: 'insert',
|
|
direction,
|
|
anchorNode,
|
|
analysis,
|
|
modelTypeLabel: 'LoRA',
|
|
});
|
|
}
|
|
|
|
export function diagnoseNode(node) {
|
|
if (!this.assistantPanelInitialized) {
|
|
this.initAssistantPanel();
|
|
}
|
|
const placeholder = document.getElementById('anomalous-assistant-placeholder');
|
|
const nodeContent = document.getElementById('anomalous-assistant-node-content');
|
|
if (!placeholder || !nodeContent || !app.graph || !app.graph._nodes) return;
|
|
|
|
if (!node) {
|
|
placeholder.innerHTML = `<div style="font-size:48px;">🤖</div><div style="text-align:center;">${t('assistantSelectNode')}</div>`;
|
|
placeholder.style.display = 'flex';
|
|
nodeContent.style.display = 'none';
|
|
nodeContent.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
const modelWidgets = [];
|
|
if (node.widgets) {
|
|
for (const w of node.widgets) {
|
|
if (w.type === 'combo' && typeof w.value === 'string') {
|
|
if (w.value.match(/\.(safetensors|ckpt|pt|bin|pth|sft)$/i)) modelWidgets.push(w);
|
|
}
|
|
}
|
|
}
|
|
|
|
const insertionCapabilities = getModelChainInsertionCapabilities(app.graph, node);
|
|
const canInsert = insertionCapabilities.before.supported || insertionCapabilities.after.supported;
|
|
|
|
const hasModelOrInsertion = modelWidgets.length > 0 || canInsert;
|
|
|
|
placeholder.style.display = 'none';
|
|
nodeContent.style.display = 'flex';
|
|
nodeContent.innerHTML = '';
|
|
|
|
const titleBar = document.createElement('div');
|
|
titleBar.style.cssText = 'margin:14px 16px 0;padding:16px;border:1px solid rgba(255,255,255,0.1);border-radius:12px;background:rgba(255,255,255,0.04);display:flex;align-items:center;gap:12px;flex-shrink:0;box-shadow:0 8px 24px rgba(0,0,0,0.25);';
|
|
titleBar.innerHTML = `<span style="width:38px;height:38px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);">🤖</span><span style="display:flex;flex-direction:column;min-width:0;gap:3px;"><span style="font-size:10px;letter-spacing:0.11em;text-transform:uppercase;color:#9ca3af;">${t('assistantSelectedNode')}</span><span class="ast-title" style="font-weight:700;color:#f3f4f6;font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></span></span><span class="ast-type" style="font-size:10px;color:#d1d5db;margin-left:auto;padding:4px 8px;border-radius:999px;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.06);max-width:38%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></span>`;
|
|
titleBar.querySelector('.ast-title').textContent = node.title || node.type || 'Node';
|
|
titleBar.querySelector('.ast-type').textContent = node.type || '';
|
|
|
|
const refreshBtn = document.createElement('button');
|
|
refreshBtn.title = t('refresh') || 'Refresh';
|
|
refreshBtn.innerHTML = '🔄';
|
|
refreshBtn.style.cssText = 'background:none; border:none; color:#c9d6ff; cursor:pointer; font-size:14px; padding:4px; margin-left:4px; border-radius:4px; transition:background 0.2s, transform 0.3s; display:flex; align-items:center; justify-content:center;';
|
|
refreshBtn.onmouseover = () => refreshBtn.style.background = 'rgba(255,255,255,0.1)';
|
|
refreshBtn.onmouseout = () => refreshBtn.style.background = 'none';
|
|
refreshBtn.onclick = () => {
|
|
refreshBtn.style.transform = 'rotate(180deg)';
|
|
setTimeout(() => this.diagnoseNode(node), 150);
|
|
};
|
|
titleBar.appendChild(refreshBtn);
|
|
|
|
nodeContent.appendChild(titleBar);
|
|
|
|
const quickActions = document.createElement('div');
|
|
quickActions.style.cssText = 'padding:14px 16px 4px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;flex-shrink:0;';
|
|
const actionsLabel = document.createElement('div');
|
|
actionsLabel.textContent = t('assistantQuickActions');
|
|
actionsLabel.style.cssText = 'grid-column:1/-1;color:#8b91a3;font-size:10px;font-weight:750;letter-spacing:0.1em;text-transform:uppercase;padding:0 2px 2px;';
|
|
quickActions.appendChild(actionsLabel);
|
|
|
|
const makeActionButton = ({ icon, label, hint, accent, onClick, capability = null, primary = false }) => {
|
|
const button = document.createElement('button');
|
|
const enabled = !capability || capability.supported;
|
|
button.disabled = !enabled;
|
|
const gridPlacement = primary ? 'grid-column:1/-1;' : '';
|
|
button.style.cssText = gridPlacement + (enabled
|
|
? `min-width:0;padding:${primary ? '13px 14px' : '11px 10px'};background:${accent};color:#fff;border:1px solid rgba(255,255,255,0.14);border-radius:11px;cursor:pointer;text-align:left;display:flex;align-items:center;gap:10px;transition:transform 0.15s,filter 0.15s,box-shadow 0.15s;box-shadow:0 8px 18px rgba(0,0,0,0.14);`
|
|
: 'min-width:0;padding:11px 10px;background:rgba(255,255,255,0.035);color:#656b78;border:1px solid rgba(255,255,255,0.055);border-radius:11px;cursor:not-allowed;text-align:left;display:flex;align-items:center;gap:9px;');
|
|
const iconEl = document.createElement('span');
|
|
iconEl.textContent = icon;
|
|
iconEl.style.cssText = `width:${primary ? '34px' : '28px'};height:${primary ? '34px' : '28px'};border-radius:9px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,${enabled ? '0.14' : '0.04'});font-size:${primary ? '17px' : '14px'};flex-shrink:0;`;
|
|
const copy = document.createElement('span');
|
|
copy.style.cssText = 'min-width:0;display:flex;flex-direction:column;gap:2px;';
|
|
const title = document.createElement('span');
|
|
title.textContent = label;
|
|
title.style.cssText = `font-weight:750;font-size:${primary ? '13px' : '11px'};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;`;
|
|
const subtitle = document.createElement('span');
|
|
subtitle.textContent = enabled ? hint : getInsertionCapabilityMessage(capability);
|
|
subtitle.style.cssText = `font-size:9px;color:${enabled ? 'rgba(255,255,255,0.68)' : '#555b66'};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;`;
|
|
copy.append(title, subtitle);
|
|
button.append(iconEl, copy);
|
|
if (enabled) {
|
|
button.onmouseover = () => { button.style.filter = 'brightness(1.12)'; button.style.transform = 'translateY(-1px)'; };
|
|
button.onmouseout = () => { button.style.filter = 'brightness(1)'; button.style.transform = 'none'; };
|
|
button.onclick = onClick;
|
|
} else if (capability) {
|
|
button.title = getInsertionCapabilityMessage(capability);
|
|
}
|
|
quickActions.appendChild(button);
|
|
};
|
|
|
|
for (const widget of modelWidgets) {
|
|
const widgetLabel = modelWidgets.length > 1 && widget.name
|
|
? t('assistantChangeWidget', { name: widget.name })
|
|
: t('assistantChangeCurrent');
|
|
const pickerType = inferPickerModelType(node, widget);
|
|
makeActionButton({
|
|
icon: '⇄',
|
|
label: widgetLabel,
|
|
hint: t('assistantVisualPicker', { type: pickerType.label }),
|
|
accent: 'linear-gradient(135deg,rgba(245,124,0,0.96),rgba(255,82,82,0.82))',
|
|
onClick: () => this._openGalleryReplacer(node, widget),
|
|
primary: true,
|
|
});
|
|
}
|
|
makeActionButton({
|
|
icon: '←',
|
|
label: t('assistantInsertBefore'),
|
|
hint: t('assistantConnectInputs'),
|
|
accent: 'linear-gradient(135deg,rgba(25,118,210,0.9),rgba(80,110,230,0.82))',
|
|
onClick: () => this.openLoraInsertionPicker(node, 'before'),
|
|
capability: insertionCapabilities.before,
|
|
});
|
|
makeActionButton({
|
|
icon: '→',
|
|
label: t('assistantInsertAfter'),
|
|
hint: t('assistantConnectOutputs'),
|
|
accent: 'linear-gradient(135deg,rgba(0,137,123,0.92),rgba(67,160,71,0.82))',
|
|
onClick: () => this.openLoraInsertionPicker(node, 'after'),
|
|
capability: insertionCapabilities.after,
|
|
});
|
|
const body = document.createElement('div');
|
|
body.className = 'anomalous-node-body';
|
|
nodeContent.appendChild(body);
|
|
if (hasModelOrInsertion) {
|
|
body.appendChild(quickActions);
|
|
for (const w of modelWidgets) this.renderAssistantModelCard(node, w, body);
|
|
}
|
|
renderNodeParameters(node, body);
|
|
}
|