Files
DemonGatanjieuandClaude Opus 5.5 2ef6555fe8 Workflows: no blank page after opening, safer card buttons, tips, narrow details, new save dialog
- Opening a workflow on the canvas only hides the browser; the Workflows list stays, so opening
  the browser again no longer shows an empty page (the old close hid the workspace without
  restoring the page under it).
- Card Open and more are two small round buttons in the cover's corner, so a press meant for the
  details does not open the canvas.
- Workflow details: the models' fold says whether all are here; folds open as the viewer last
  left them; a tip when a workflow has no set (with Save the canvas) and one pointing at compare
  on a set until used. Narrow (docked) details turn the left column into a row of pills.
- Save dialog rebuilt in ui_recipe_save_dialog.js: cover on the left from the canvas, the current
  cover or recent pictures as thumbnails; fields on the right; Cancel and Save always in view; the
  name starts as the canvas's saved workflow name; Esc and Enter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:48:50 +08:00

124 lines
6.0 KiB
JavaScript

/** The edit dialog for Workflow Recipes (name, tags, notes, versions); saving is ui_recipe_save_dialog.js. */
import { translate } from "./locales.js";
import { anomalousAlert, anomalousConfirm } from "./ui_dialog.js";
import { appendText } from "./ui_recipe_detail_dom.js";
const t = (key, params) => translate(key, params);
export function formatRecipeText(key, values = {}) {
return Object.entries(values).reduce((text, [name, value]) => text.replaceAll(`{${name}}`, String(value)), t(key));
}
export function showRecipeEditDialog(owner, recipeData, filename, history) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.className = 'anomalous-recipe-dialog-overlay';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
const dialog = document.createElement('div');
dialog.className = 'anomalous-recipe-dialog anomalous-recipe-edit-dialog';
appendText(dialog, 'h3', t('recipeEditTitle'));
const nameLabel = appendText(dialog, 'label', t('recipeName'));
const nameInput = document.createElement('input');
nameInput.className = 'anomalous-nb-select';
nameInput.type = 'text';
nameInput.maxLength = 120;
nameInput.value = recipeData.name || '';
nameLabel.appendChild(nameInput);
const tagsLabel = appendText(dialog, 'label', t('recipeTags'));
const tagsInput = document.createElement('input');
tagsInput.className = 'anomalous-nb-select';
tagsInput.type = 'text';
tagsInput.maxLength = 300;
tagsInput.value = Array.isArray(recipeData.tags) ? recipeData.tags.join(', ') : '';
tagsInput.placeholder = t('recipeTagsHint');
tagsLabel.appendChild(tagsInput);
const notesLabel = appendText(dialog, 'label', t('recipeNotes'));
const notesInput = document.createElement('textarea');
notesInput.className = 'anomalous-nb-textarea';
notesInput.maxLength = 3000;
notesInput.value = recipeData.notes || '';
notesInput.placeholder = t('recipeNotesHint');
notesLabel.appendChild(notesInput);
const historyDetails = document.createElement('details');
historyDetails.className = 'anomalous-recipe-node-details';
const historySummary = document.createElement('summary');
historySummary.textContent = `${t('recipeHistory')} (${history.length})`;
historyDetails.appendChild(historySummary);
const historyList = document.createElement('div');
historyList.className = 'anomalous-recipe-history-list';
if (!history.length) {
appendText(historyList, 'small', t('recipeHistoryEmpty'), 'anomalous-recipe-node-hint');
} else {
for (const version of history) {
const row = document.createElement('div');
row.className = 'anomalous-recipe-history-row';
const date = Number.isFinite(Number(version.timestamp))
? new Date(Number(version.timestamp)).toLocaleString()
: t('recipeUnknownVersion');
appendText(row, 'span', `${date} · ${version.name || t('recipeUntitled')}`);
const restore = appendText(row, 'button', t('recipeRestoreVersion'), 'anomalous-btn-danger');
restore.type = 'button';
restore.onclick = async () => {
if (!await anomalousConfirm(t('recipeRestoreVersionConfirm'))) return;
try {
const response = await fetch('/anomalous/restore_recipe_version', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename, version: version.version }),
});
if (!response.ok) throw new Error('recipe history restore failed');
overlay.remove();
await owner.refreshRecipes();
resolve({ mode: 'restored' });
} catch (error) {
console.error('Could not restore Workflow Recipe version:', error);
await anomalousAlert(t('recipeUpdateError'));
}
};
row.appendChild(restore);
historyList.appendChild(row);
}
}
historyDetails.appendChild(historyList);
dialog.appendChild(historyDetails);
const error = appendText(dialog, 'div', '', 'anomalous-recipe-dialog-error');
const actions = document.createElement('div');
actions.className = 'anomalous-recipe-actions';
const cancel = appendText(actions, 'button', t('recipeCancel'), 'anomalous-btn-danger');
const canvas = appendText(actions, 'button', t('recipeEditCanvas'), 'anomalous-btn-primary');
const save = appendText(actions, 'button', t('recipeUpdate'), 'anomalous-btn-success');
for (const button of [cancel, canvas, save]) button.type = 'button';
const close = (value) => { overlay.remove(); resolve(value); };
cancel.onclick = () => close(null);
overlay.onclick = (event) => { if (event.target === overlay) close(null); };
canvas.onclick = () => close({ mode: 'canvas' });
save.onclick = () => {
const name = nameInput.value.trim();
if (!name) {
error.textContent = t('recipeNameRequired');
nameInput.focus();
return;
}
close({
mode: 'save',
name,
tags: [...new Set(tagsInput.value.split(',').map((tag) => tag.trim()).filter(Boolean))].slice(0, 20),
notes: notesInput.value.trim(),
});
};
dialog.appendChild(actions);
overlay.appendChild(dialog);
(owner.nbPanel || document.body).appendChild(overlay);
nameInput.focus();
nameInput.select();
});
}