The details open on the workflow's values instead of an overview. A bar (Back, the name edited in place, the models' state, Open, ⋯ edit and export); a left column (ui_recipe_sets.js): the workflow's own values, the saved sets unlike them (each with its key values and difference count), Save the canvas, New, then all pictures and versions; the chosen view on the right (ui_recipe_set_view.js): Apply to canvas, ⇄ with the default values or the canvas, ⋯ copy / rename / delete, the main values, prompts (role menu, folded when long), the pictures made with these values, and folded models and nodes. ui_recipe_overview.js gives way to ui_recipe_info.js (name, notes, tags, cover, models' state); ui_recipe_parameters.js keeps prompt roles, node values and the set editor, and loses its 600-line tab renderer. The pictures (ui_recipe_gallery.js) load as thumbnails and each has ⇄: the pick bar from ui_param_compare.js now goes wherever the pictures are and can offer the workflow's own values as a side. Also: applyRecipeToCanvas used anomalousAlert without importing it. The old tab, hero, bento, console and gallery-dialog rules are pruned (about 1,000 lines). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
7.1 KiB
JavaScript
174 lines
7.1 KiB
JavaScript
/**
|
|
* A workflow's own information in its details: the name in the bar, the models' state as a
|
|
* dot and a word, and the cover, notes and tags atop its default values (ui_recipe_set_view.js).
|
|
* Name, notes and tags are edited in place (Enter or leaving the box saves, Esc keeps them).
|
|
*/
|
|
|
|
import { translate } from "./locales.js";
|
|
import { appendCopyButton, appendText, button, dateText } from "./ui_recipe_detail_dom.js";
|
|
import { appendRecipeCover } from "./ui_recipe_models.js";
|
|
import { updateRecipeMetadata } from "./ui_recipe_metadata.js";
|
|
|
|
const t = (key, params) => translate(key, params);
|
|
|
|
export function recipeCanvasActionLabel(recipe) {
|
|
return t(recipe?.workflow_scope === 'partial' ? 'recipeAppendCanvas' : 'recipeOpenCanvas');
|
|
}
|
|
function beginInlineEdit(owner, recipe, container, field, renderValue, options = {}) {
|
|
const editor = document.createElement('div');
|
|
editor.className = `anomalous-recipe-inline-editor${options.multiline ? ' is-multiline' : ''}`;
|
|
const input = document.createElement(options.multiline ? 'textarea' : 'input');
|
|
input.className = 'anomalous-recipe-inline-input';
|
|
input.value = Array.isArray(recipe[field]) ? recipe[field].join(', ') : String(recipe[field] || '');
|
|
if (!options.multiline) input.type = 'text';
|
|
if (options.maxLength) input.maxLength = options.maxLength;
|
|
if (options.multiline) input.rows = Math.max(3, Math.min(10, input.value.split('\n').length));
|
|
|
|
let finished = false;
|
|
|
|
const restore = () => {
|
|
if (finished) return;
|
|
finished = true;
|
|
renderValue(container);
|
|
};
|
|
|
|
const commit = async () => {
|
|
if (finished) return;
|
|
const raw = input.value.trim();
|
|
const value = options.parse ? options.parse(raw) : raw;
|
|
if (options.required && !value) {
|
|
input.focus();
|
|
return;
|
|
}
|
|
finished = true;
|
|
input.disabled = true;
|
|
|
|
// Simple visual feedback during save
|
|
input.style.opacity = '0.5';
|
|
try {
|
|
await updateRecipeMetadata(owner, recipe, { [field]: value });
|
|
renderValue(container);
|
|
} catch (error) {
|
|
console.error('Could not update inline recipe metadata:', error);
|
|
finished = false;
|
|
input.disabled = false;
|
|
input.style.opacity = '1';
|
|
input.focus();
|
|
}
|
|
};
|
|
|
|
input.addEventListener('blur', () => void commit());
|
|
input.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
restore();
|
|
} else if (event.key === 'Enter' && (!options.multiline || event.ctrlKey || event.metaKey)) {
|
|
event.preventDefault();
|
|
void commit();
|
|
}
|
|
});
|
|
|
|
editor.appendChild(input);
|
|
container.replaceChildren(editor);
|
|
input.focus();
|
|
input.setSelectionRange(input.value.length, input.value.length);
|
|
}
|
|
|
|
function missingNodeTypes(recipe) {
|
|
const registry = globalThis.LiteGraph?.registered_node_types;
|
|
if (!registry) return [];
|
|
return [...new Set((recipe?.workflow?.nodes || [])
|
|
.map((node) => node?.type || node?.class_type)
|
|
.filter((type) => type && !registry[type]))];
|
|
}
|
|
|
|
|
|
export function renderInlineTitle(parent, owner, recipe) {
|
|
parent.replaceChildren();
|
|
const title = button(parent, recipe.name || t('recipeUntitled'), 'anomalous-recipe-inline-title');
|
|
title.title = t('recipeInlineEditName');
|
|
title.onclick = () => beginInlineEdit(
|
|
owner,
|
|
recipe,
|
|
parent,
|
|
'name',
|
|
(target) => renderInlineTitle(target, owner, recipe),
|
|
{ maxLength: 120, required: true },
|
|
);
|
|
}
|
|
|
|
function renderInlineNotes(parent, owner, recipe) {
|
|
parent.replaceChildren();
|
|
const notes = appendText(
|
|
parent,
|
|
'p',
|
|
recipe.notes || t('recipeDetailNoNotes'),
|
|
'anomalous-recipe-detail-muted anomalous-recipe-inline-editable',
|
|
);
|
|
notes.title = t('recipeInlineEditNotes');
|
|
notes.onclick = () => beginInlineEdit(
|
|
owner,
|
|
recipe,
|
|
parent,
|
|
'notes',
|
|
(target) => renderInlineNotes(target, owner, recipe),
|
|
{ multiline: true, maxLength: 5000 },
|
|
);
|
|
if (recipe.notes) appendCopyButton(parent, recipe.notes);
|
|
}
|
|
|
|
function renderInlineTags(parent, owner, recipe) {
|
|
parent.replaceChildren();
|
|
for (const tag of recipe.tags || []) appendText(parent, 'span', tag, 'anomalous-recipe-badge anomalous-recipe-badge-tag');
|
|
const edit = button(parent, t('recipeInlineEditTags'), 'anomalous-recipe-inline-edit-button');
|
|
edit.onclick = () => beginInlineEdit(
|
|
owner,
|
|
recipe,
|
|
parent,
|
|
'tags',
|
|
(target) => renderInlineTags(target, owner, recipe),
|
|
{
|
|
maxLength: 300,
|
|
parse: (value) => [...new Set(value.split(',').map((tag) => tag.trim()).filter(Boolean))].slice(0, 20),
|
|
},
|
|
);
|
|
}
|
|
|
|
/** The models' state: a dot and a word; what is missing, and why it matters, in its tooltip. */
|
|
export function renderReadiness(parent, recipe, references) {
|
|
const checked = references.some((ref) => ref.currentAvailability);
|
|
const missing = references.filter((ref) => ref.currentAvailability === 'missing').length;
|
|
const missingNodes = missingNodeTypes(recipe);
|
|
const state = !references.length ? 'ready' : !checked ? 'checking' : missing ? 'missing' : 'ready';
|
|
const key = !references.length ? 'recipeStatusNoModels' : { checking: 'recipeStatusChecking', missing: 'recipeStatusMissing', ready: 'recipeStatusReady' }[state];
|
|
const line = document.createElement('span');
|
|
line.className = `anomalous-wfd-status is-${missingNodes.length && state === 'ready' ? 'missing' : state}`;
|
|
appendText(line, 'span', '', 'anomalous-wfd-status-dot');
|
|
const label = missingNodes.length && state === 'ready' ? t('recipeMissingNodes') : t(key, { count: missing });
|
|
appendText(line, 'span', label, 'anomalous-wfd-status-text');
|
|
const notes = [missing ? t('recipeMissingHint') : '', missingNodes.length ? `${t('recipeMissingNodes')}: ${missingNodes.join(', ')}` : ''];
|
|
line.title = notes.filter(Boolean).join('\n') || label;
|
|
parent.appendChild(line);
|
|
return line;
|
|
}
|
|
|
|
/** Cover, notes and tags (each edited in place) and when it was last saved. */
|
|
export function renderRecipeInfo(parent, owner, recipe) {
|
|
const info = document.createElement('div');
|
|
info.className = 'anomalous-wfd-info';
|
|
const cover = document.createElement('div');
|
|
cover.className = 'anomalous-wfd-cover';
|
|
appendRecipeCover(cover, owner, recipe);
|
|
const text = document.createElement('div');
|
|
text.className = 'anomalous-wfd-info-text';
|
|
const notes = document.createElement('div');
|
|
notes.className = 'anomalous-recipe-detail-notes';
|
|
renderInlineNotes(notes, owner, recipe);
|
|
const tags = document.createElement('div');
|
|
tags.className = 'anomalous-recipe-tags anomalous-recipe-detail-tags';
|
|
renderInlineTags(tags, owner, recipe);
|
|
text.append(notes, tags);
|
|
appendText(text, 'small', `${t('recipeDetailUpdated')}: ${dateText(recipe.updated_timestamp || recipe.timestamp)}`, 'anomalous-wfd-muted');
|
|
info.append(cover, text);
|
|
parent.appendChild(info);
|
|
} |