feat: refactor model composition and implement modern custom dialogs for recipes

This commit is contained in:
DemonGatanjieu
2026-08-03 05:08:47 +08:00
parent 6a91a18fb4
commit c217595f4e
3 changed files with 173 additions and 68 deletions
+138
View File
@@ -0,0 +1,138 @@
export function anomalousAlert(message, title = 'Anomalous') {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.inset = '0';
overlay.style.zIndex = '999999';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.background = 'rgba(0, 0, 0, 0.6)';
overlay.style.backdropFilter = 'blur(4px)';
overlay.style.padding = '20px';
overlay.style.boxSizing = 'border-box';
const dialog = document.createElement('div');
dialog.style.background = 'linear-gradient(145deg, rgba(48, 49, 55, 0.98), rgba(27, 28, 33, 0.98))';
dialog.style.border = '1px solid rgba(255, 255, 255, 0.12)';
dialog.style.borderRadius = '16px';
dialog.style.padding = '24px';
dialog.style.maxWidth = '400px';
dialog.style.width = '100%';
dialog.style.maxHeight = '90vh';
dialog.style.overflowY = 'auto';
dialog.style.boxShadow = '0 24px 70px rgba(0, 0, 0, 0.55)';
dialog.style.color = '#f3f4f6';
dialog.style.display = 'flex';
dialog.style.flexDirection = 'column';
dialog.style.gap = '16px';
const heading = document.createElement('h3');
heading.textContent = title;
heading.style.margin = '0';
heading.style.fontSize = '1.25rem';
const text = document.createElement('p');
text.textContent = message;
text.style.margin = '0';
text.style.lineHeight = '1.5';
text.style.whiteSpace = 'pre-wrap';
text.style.color = '#ccc';
const footer = document.createElement('div');
footer.style.display = 'flex';
footer.style.justifyContent = 'flex-end';
footer.style.marginTop = '8px';
const okBtn = document.createElement('button');
okBtn.textContent = window.anomalous_browser_lang === 'zh' ? '确定' : 'OK';
okBtn.className = 'anomalous-btn-primary';
okBtn.style.padding = '8px 24px';
const cleanup = () => {
overlay.remove();
resolve();
};
okBtn.onclick = cleanup;
footer.appendChild(okBtn);
dialog.append(heading, text, footer);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
okBtn.focus();
});
}
export function anomalousConfirm(message, title = 'Anomalous') {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.inset = '0';
overlay.style.zIndex = '999999';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.background = 'rgba(0, 0, 0, 0.6)';
overlay.style.backdropFilter = 'blur(4px)';
overlay.style.padding = '20px';
overlay.style.boxSizing = 'border-box';
const dialog = document.createElement('div');
dialog.style.background = 'linear-gradient(145deg, rgba(48, 49, 55, 0.98), rgba(27, 28, 33, 0.98))';
dialog.style.border = '1px solid rgba(255, 255, 255, 0.12)';
dialog.style.borderRadius = '16px';
dialog.style.padding = '24px';
dialog.style.maxWidth = '400px';
dialog.style.width = '100%';
dialog.style.maxHeight = '90vh';
dialog.style.overflowY = 'auto';
dialog.style.boxShadow = '0 24px 70px rgba(0, 0, 0, 0.55)';
dialog.style.color = '#f3f4f6';
dialog.style.display = 'flex';
dialog.style.flexDirection = 'column';
dialog.style.gap = '16px';
const heading = document.createElement('h3');
heading.textContent = title;
heading.style.margin = '0';
heading.style.fontSize = '1.25rem';
const text = document.createElement('p');
text.textContent = message;
text.style.margin = '0';
text.style.lineHeight = '1.5';
text.style.whiteSpace = 'pre-wrap';
text.style.color = '#ccc';
const footer = document.createElement('div');
footer.style.display = 'flex';
footer.style.justifyContent = 'flex-end';
footer.style.gap = '12px';
footer.style.marginTop = '8px';
const cancelBtn = document.createElement('button');
cancelBtn.textContent = window.anomalous_browser_lang === 'zh' ? '取消' : 'Cancel';
cancelBtn.className = 'anomalous-btn-ghost';
cancelBtn.style.padding = '8px 24px';
const okBtn = document.createElement('button');
okBtn.textContent = window.anomalous_browser_lang === 'zh' ? '确定' : 'OK';
okBtn.className = 'anomalous-btn-danger';
okBtn.style.padding = '8px 24px';
const finish = (result) => {
overlay.remove();
resolve(result);
};
cancelBtn.onclick = () => finish(false);
okBtn.onclick = () => finish(true);
footer.append(cancelBtn, okBtn);
dialog.append(heading, text, footer);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
cancelBtn.focus();
});
}
+19 -53
View File
@@ -1,5 +1,6 @@
import { app } from '../../../scripts/app.js';
import { i18n } from './locales.js';
import { i18n } from './locales.js';
import { anomalousAlert, anomalousConfirm } from './ui_dialog.js';
import {
deriveRecipeModelReferences,
formatIdentitySize,
@@ -66,7 +67,7 @@ export async function openRecipeOnCanvas(owner, recipe) {
const missingWarning = missingTypes.length
? `\n\n${t('recipeMissingNodes')}:\n${missingTypes.slice(0, 12).join('\n')}`
: '';
if ((canvasMayContainUserWork() || missingWarning) && !confirm(`${t('recipeOpenCanvasConfirm')}${missingWarning}`)) return false;
if ((canvasMayContainUserWork() || missingWarning) && !await anomalousConfirm(`${t('recipeOpenCanvasConfirm')}${missingWarning}`)) return false;
try {
await app.loadGraphData(recipe.workflow);
app.canvas?.setDirty?.(true, true);
@@ -74,7 +75,7 @@ export async function openRecipeOnCanvas(owner, recipe) {
return true;
} catch (error) {
console.error('Could not open Workflow Recipe on canvas:', error);
alert(t('recipeRestoreError'));
await anomalousAlert(t('recipeRestoreError'));
return false;
}
}
@@ -86,7 +87,7 @@ export function appendRecipeOnCanvas(owner, recipe) {
return true;
} catch (error) {
console.error('Could not append Workflow Recipe:', error);
alert(error.code === 'recipe_append_missing_node'
await anomalousAlert(error.code === 'recipe_append_missing_node'
? `${t('recipeAppendError')}\n${error.message}`
: t('recipeAppendError'));
return false;
@@ -657,39 +658,8 @@ function renderOverview(content, owner, recipe, references, finish) {
const params = recipe.params || {};
const modelComposition = document.createElement('div');
modelComposition.className = 'anomalous-recipe-model-composition';
appendText(modelComposition, 'h5', t('recipeDetailModelComposition'));
const modelCompositionGrid = document.createElement('div');
modelCompositionGrid.className = 'anomalous-recipe-model-composition-grid';
const baseModelCard = document.createElement('article');
baseModelCard.className = 'anomalous-recipe-model-composition-card is-base-model';
appendText(baseModelCard, 'span', t('recipeDetailBaseModel'), 'anomalous-recipe-detail-label');
appendValueViewer(
baseModelCard,
modelDisplayName(params.baseModel) || t('recipeDetailUnavailable'),
'anomalous-recipe-model-composition-value',
);
modelCompositionGrid.appendChild(baseModelCard);
const loras = Array.isArray(params.loras) ? params.loras : [];
if (loras.length) {
loras.forEach((lora, index) => {
const loraCard = document.createElement('article');
loraCard.className = 'anomalous-recipe-model-composition-card';
appendText(loraCard, 'span', `${t('recipeDetailLoraSummary')} ${index + 1}`, 'anomalous-recipe-detail-label');
appendValueViewer(
loraCard,
modelDisplayName(lora?.name) || t('recipeDetailUnavailable'),
'anomalous-recipe-model-composition-value',
);
if (lora?.strength_model !== null && lora?.strength_model !== undefined) {
appendText(loraCard, 'small', `× ${lora.strength_model}`, 'anomalous-recipe-detail-muted');
}
modelCompositionGrid.appendChild(loraCard);
});
} else {
appendText(modelCompositionGrid, 'small', `${t('recipeDetailLoraSummary')}: ${t('recipeDetailUnavailable')}`, 'anomalous-recipe-detail-muted');
}
modelComposition.appendChild(modelCompositionGrid);
summary.appendChild(modelComposition);
renderModelComposition(modelComposition, owner, recipe, references, finish, params);
const values = [
[t('recipeSteps'), params.steps],
['CFG', params.cfg],
@@ -733,12 +703,11 @@ function renderOverview(content, owner, recipe, references, finish) {
content.appendChild(overview);
}
function renderModels(content, owner, recipe, references, finish) {
const section = document.createElement('section');
section.className = 'anomalous-recipe-detail-section';
function renderModelComposition(container, owner, recipe, references, finish, params) {
container.replaceChildren();
const heading = document.createElement('div');
heading.className = 'anomalous-recipe-detail-section-heading';
appendText(heading, 'h4', t('recipeDetailModels'));
appendText(heading, 'h5', t('recipeDetailModelComposition'));
const refresh = button(heading, t('recipeDetailRefreshAvailability'), 'anomalous-btn-primary');
const status = appendText(
heading,
@@ -770,18 +739,16 @@ function renderModels(content, owner, recipe, references, finish) {
}
}
await loadCurrentPreviews(owner, references);
content.replaceChildren();
renderModels(content, owner, recipe, references, finish);
renderModelComposition(container, owner, recipe, references, finish, params);
} catch (error) {
console.error('Could not refresh recipe model availability:', error);
status.textContent = t('recipeDetailRefreshError');
refresh.disabled = false;
}
};
section.appendChild(heading);
container.appendChild(heading);
if (!references.length) {
appendText(section, 'p', t('recipeDetailNoModelReferences'), 'anomalous-recipe-detail-muted');
content.appendChild(section);
appendText(container, 'p', t('recipeDetailNoModelReferences'), 'anomalous-recipe-detail-muted');
return;
}
const list = document.createElement('div');
@@ -789,7 +756,8 @@ function renderModels(content, owner, recipe, references, finish) {
for (const reference of references) {
const card = document.createElement('article');
const isLocal = Boolean(reference.localModel);
card.className = `anomalous-recipe-model-reference${isLocal ? ' is-local' : ' is-unresolved'}`;
const isBase = params && params.baseModel && reference.saved_value === params.baseModel;
card.className = `anomalous-recipe-model-reference${isLocal ? ' is-local' : ' is-unresolved'}${isBase ? ' is-base-model' : ''}`;
const body = document.createElement('div');
body.className = 'anomalous-recipe-model-reference-body';
const openModel = isLocal
@@ -864,8 +832,7 @@ function renderModels(content, owner, recipe, references, finish) {
match.onclick = async () => {
match.disabled = true;
await matchLocalModel(owner, recipe, reference, matchStatus, () => {
content.replaceChildren();
renderModels(content, owner, recipe, references, finish);
renderModelComposition(container, owner, recipe, references, finish, params);
});
if (!reference.localModel) match.disabled = false;
};
@@ -875,8 +842,7 @@ function renderModels(content, owner, recipe, references, finish) {
card.appendChild(body);
list.appendChild(card);
}
section.appendChild(list);
content.appendChild(section);
container.appendChild(list);
}
function parameterNodesWithPromptFallback(recipe) {
@@ -1096,7 +1062,7 @@ function renderVersions(content, owner, recipe, history, finish) {
};
const restore = button(row, t('recipeRestoreVersion'), 'anomalous-btn-danger');
restore.onclick = async () => {
if (!confirm(t('recipeRestoreVersionConfirm'))) return;
if (!await anomalousConfirm(t('recipeRestoreVersionConfirm'))) return;
try {
const response = await fetch('/anomalous/restore_recipe_version', {
method: 'POST',
@@ -1108,7 +1074,7 @@ function renderVersions(content, owner, recipe, history, finish) {
finish('restored');
} catch (error) {
console.error('Could not restore recipe version:', error);
alert(t('recipeUpdateError'));
await anomalousAlert(t('recipeUpdateError'));
}
};
timeline.appendChild(row);
@@ -1187,7 +1153,7 @@ export function showRecipeDetail(owner, { recipe, filename, history = [] }) {
const tabDefinitions = [
['overview', t('recipeDetailOverview'), () => {
renderOverview(content, owner, recipe, references, finish);
renderModels(content, owner, recipe, references, finish);
const paramsWrapper = document.createElement('details');
paramsWrapper.className = 'anomalous-recipe-detail-section';
const summary = document.createElement('summary');
+16 -15
View File
@@ -1,7 +1,8 @@
/** Workflow Recipes UI, built on the same modal/card language as Notebooks. */
import { app } from '../../../scripts/app.js';
import { i18n } from './locales.js';
import { i18n } from './locales.js';
import { anomalousAlert, anomalousConfirm } from './ui_dialog.js';
import {
captureCanvasThumbnail,
captureRecipeDraft,
@@ -32,7 +33,7 @@ async function runRecipeCardAction(actionButton, action, errorKey) {
await action();
} catch (error) {
console.error('Workflow Recipe action failed:', error);
alert(t(errorKey));
await anomalousAlert(t(errorKey));
} finally {
actionButton.disabled = false;
actionButton.classList.remove('is-busy');
@@ -108,9 +109,9 @@ function appendRecipeCover(parent, url, alt) {
}
async function exportRecipePackage(filename) {
const includeSnapshots = confirm(t('recipeExportSnapshotsConfirm'));
const includeHistory = confirm(t('recipeExportHistoryConfirm'));
const includeIdentity = !confirm(t('recipeExportRedactIdentityConfirm'));
const includeSnapshots = await anomalousConfirm(t('recipeExportSnapshotsConfirm'));
const includeHistory = await anomalousConfirm(t('recipeExportHistoryConfirm'));
const includeIdentity = !await anomalousConfirm(t('recipeExportRedactIdentityConfirm'));
const response = await fetch('/anomalous/export_recipe_package', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -143,7 +144,7 @@ async function importRecipePackage(owner, file) {
if (!inspectResponse.ok || inspectPayload.status !== 'success') throw new Error('recipe import inspection failed');
const recipeName = inspectPayload.recipe?.name || t('recipeUntitled');
const summary = `${t('recipeImportSummary')}\n\n${recipeName}\n${t('recipeImportAssets')}: ${inspectPayload.asset_count || 0}\n${t('recipeImportHistory')}: ${inspectPayload.history_count || 0}`;
if (!confirm(summary)) return;
if (!await anomalousConfirm(summary)) return;
let name = recipeName;
if ((inspectPayload.existing_names || []).includes(name)) {
name = prompt(t('recipeImportRenamePrompt'), `${name} (Imported)`);
@@ -418,7 +419,7 @@ async function editRecipe(owner, recipe, filename, history = null) {
const result = await showRecipeEditDialog(owner, editable, filename, bundle.history);
if (!result || result.mode === 'restored') return;
if (result.mode === 'canvas') {
if (!confirm(t('recipeEditCanvasConfirm'))) return;
if (!await anomalousConfirm(t('recipeEditCanvasConfirm'))) return;
app.loadGraphData(editable.workflow);
app.canvas?.setDirty?.(true, true);
owner.recipeEditing = { filename, data: editable };
@@ -443,7 +444,7 @@ async function editRecipe(owner, recipe, filename, history = null) {
await owner.refreshRecipes();
} catch (error) {
console.error('Could not edit Workflow Recipe:', error);
alert(t('recipeUpdateError'));
await anomalousAlert(t('recipeUpdateError'));
}
}
@@ -839,7 +840,7 @@ function showRecipeEditDialog(owner, recipeData, filename, history) {
const restore = appendText(row, 'button', t('recipeRestoreVersion'), 'anomalous-btn-danger');
restore.type = 'button';
restore.onclick = async () => {
if (!confirm(t('recipeRestoreVersionConfirm'))) return;
if (!await anomalousConfirm(t('recipeRestoreVersionConfirm'))) return;
try {
const response = await fetch('/anomalous/restore_recipe_version', {
method: 'POST',
@@ -852,7 +853,7 @@ function showRecipeEditDialog(owner, recipeData, filename, history) {
resolve({ mode: 'restored' });
} catch (error) {
console.error('Could not restore Workflow Recipe version:', error);
alert(t('recipeUpdateError'));
await anomalousAlert(t('recipeUpdateError'));
}
};
row.appendChild(restore);
@@ -971,7 +972,7 @@ export async function showRecipes() {
await importRecipePackage(this, file);
} catch (error) {
console.error('Could not import Workflow Recipe package:', error);
alert(t('recipeImportError'));
await anomalousAlert(t('recipeImportError'));
}
};
input.click();
@@ -1084,7 +1085,7 @@ export function renderRecipeList(recipes) {
remove.onclick = (e) => {
e.stopPropagation();
runRecipeCardAction(remove, async () => {
if (!confirm(t('recipeDeleteConfirm'))) return;
if (!await anomalousConfirm(t('recipeDeleteConfirm'))) return;
const response = await fetch('/anomalous/delete_recipe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -1124,12 +1125,12 @@ export function renderRecipeList(recipes) {
export async function handleSaveRecipe() {
if (!app.graph?.serialize) {
alert(t('recipeSaveError'));
await anomalousAlert(t('recipeSaveError'));
return;
}
const draft = captureRecipeDraft(app.graph);
if (!draft.workflow || !Array.isArray(draft.workflow.nodes)) {
alert(t('recipeSaveError'));
await anomalousAlert(t('recipeSaveError'));
return;
}
const canvasThumbnail = captureCanvasThumbnail(app.canvas?.canvas);
@@ -1187,7 +1188,7 @@ export async function handleSaveRecipe() {
await this.refreshRecipes();
} catch (error) {
console.error('Could not save Workflow Recipe:', error);
alert(t('recipeSaveError'));
await anomalousAlert(t('recipeSaveError'));
} finally {
if (saveButton) {
saveButton.disabled = false;