Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_materials.js
T

242 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Curated material snapshots: generated image + exact workflow + node blocks. */
import { app } from '../../../scripts/app.js';
import { translate } from './locales.js';
import { anomalousAlert, anomalousConfirm } from './ui_dialog.js';
const t = (key, params) => translate(key, params);
function text(parent, tag, value, className = '') {
const element = document.createElement(tag);
if (className) element.className = className;
element.textContent = value == null ? '' : String(value);
parent.appendChild(element);
return element;
}
function materialAssetUrl(filename, asset) {
if (!filename || !asset) return '';
return `/anomalous/material_asset?filename=${encodeURIComponent(filename)}&asset=${encodeURIComponent(asset)}`;
}
async function jsonResponse(response, fallbackMessage) {
let payload = {};
try { payload = await response.json(); } catch (error) { /* non-JSON server failure */ }
if (!response.ok) throw new Error(payload.message || fallbackMessage);
return payload;
}
function hideSiblingWorkspaceViews(owner) {
if (owner.notebookBody) owner.notebookBody.style.display = 'none';
if (owner.recipeView) owner.recipeView.style.display = 'none';
owner.notebookNotesTab?.classList.remove('active');
owner.notebookRecipesTab?.classList.remove('active');
owner.notebookMaterialsTab?.classList.add('active');
}
async function fetchMaterial(filename) {
const response = await fetch(`/anomalous/material_full?filename=${encodeURIComponent(filename)}`, { cache: 'no-store' });
const payload = await jsonResponse(response, 'material load failed');
if (payload.status !== 'success') throw new Error(payload.message || 'material load failed');
return payload;
}
async function openMaterialWorkflow(owner, filename) {
const payload = await fetchMaterial(filename);
if (!payload.data?.workflow || typeof app.loadGraphData !== 'function') throw new Error('material workflow unavailable');
await app.loadGraphData(JSON.parse(JSON.stringify(payload.data.workflow)));
app.canvas?.setDirty?.(true, true);
owner.closeWorkspace?.();
owner.close?.();
window.setTimeout(() => window.anomalous_resolve_all_missing_nodes?.(true, false), 0);
}
function renderMaterialCard(owner, material) {
const card = document.createElement('article');
card.className = 'anomalous-material-card';
const preview = document.createElement('div');
preview.className = 'anomalous-material-card-preview';
const previewUrl = materialAssetUrl(material.filename, material.image?.preview_asset_id || material.image?.source_asset_id);
if (previewUrl) {
const image = document.createElement('img');
image.src = previewUrl;
image.alt = material.name || t('materialUntitled');
image.loading = 'lazy';
preview.appendChild(image);
} else {
preview.textContent = '🖼️';
}
card.appendChild(preview);
const body = document.createElement('div');
body.className = 'anomalous-material-card-body';
text(body, 'h3', material.name || t('materialUntitled'));
text(body, 'p', t('materialNodeSummary', { count: material.node_count || 0 }), 'anomalous-material-muted');
if (Array.isArray(material.node_types) && material.node_types.length) {
text(body, 'small', material.node_types.slice(0, 5).join(' · '), 'anomalous-material-types');
}
card.appendChild(body);
const actions = document.createElement('div');
actions.className = 'anomalous-material-card-actions';
const open = text(actions, 'button', `🚀 ${t('materialOpenWorkflow')}`, 'anomalous-btn-primary');
open.type = 'button';
open.onclick = async () => {
open.disabled = true;
try {
await openMaterialWorkflow(owner, material.filename);
} catch (error) {
console.error('Could not open material workflow:', error);
await anomalousAlert(t('materialOpenError'));
open.disabled = false;
}
};
const remove = text(actions, 'button', '🗑️', 'anomalous-btn-ghost');
remove.type = 'button';
remove.title = t('materialDelete');
remove.onclick = async () => {
if (!await anomalousConfirm(t('materialDeleteConfirm'))) return;
const response = await fetch('/anomalous/delete_material', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: material.filename }),
});
if (!response.ok) {
await anomalousAlert(t('materialDeleteError'));
return;
}
await owner.refreshMaterials?.();
};
card.appendChild(actions);
return card;
}
export async function refreshMaterials() {
if (!this.materialList) return;
this.materialList.replaceChildren();
text(this.materialList, 'p', t('loading'), 'anomalous-material-empty');
try {
const response = await fetch('/anomalous/materials', { cache: 'no-store' });
const payload = await jsonResponse(response, 'material list failed');
this.materialList.replaceChildren();
const materials = Array.isArray(payload.materials) ? payload.materials : [];
if (!materials.length) {
text(this.materialList, 'p', t('materialEmpty'), 'anomalous-material-empty');
return;
}
for (const material of materials) this.materialList.appendChild(renderMaterialCard(this, material));
} catch (error) {
console.error('Could not load materials:', error);
this.materialList.replaceChildren();
text(this.materialList, 'p', t('materialLoadError'), 'anomalous-material-empty');
}
}
export async function showMaterials() {
if (!this.notebookContainer) {
this.nbPanel.style.display = 'flex';
await this.showNotebooks();
}
hideSiblingWorkspaceViews(this);
if (!this.materialView) {
this.materialView = document.createElement('div');
this.materialView.className = 'anomalous-material-body';
const intro = document.createElement('div');
intro.className = 'anomalous-material-intro';
const introCopy = document.createElement('div');
text(introCopy, 'h3', t('materialLibrary'));
text(introCopy, 'p', t('materialLibraryHint'), 'anomalous-material-muted');
const refresh = text(intro, 'button', `↻ ${t('refresh')}`, 'anomalous-btn-ghost');
refresh.type = 'button';
refresh.onclick = () => this.refreshMaterials();
intro.prepend(introCopy);
this.materialView.appendChild(intro);
this.materialList = document.createElement('div');
this.materialList.className = 'anomalous-material-list';
this.materialView.appendChild(this.materialList);
this.notebookContainer.appendChild(this.materialView);
}
this.materialView.style.display = 'flex';
await this.refreshMaterials();
}
export async function showImageMaterialDetail(owner, sourceImage, imageUrl) {
const overlay = document.createElement('div');
overlay.className = 'anomalous-material-detail-overlay';
const dialog = document.createElement('div');
dialog.className = 'anomalous-material-detail-dialog';
const close = text(dialog, 'button', '×', 'anomalous-material-detail-close');
close.type = 'button';
close.onclick = () => overlay.remove();
const media = document.createElement('div');
media.className = 'anomalous-material-detail-media';
const image = document.createElement('img');
image.src = imageUrl;
image.alt = t('materialImageDetail');
media.appendChild(image);
const side = document.createElement('div');
side.className = 'anomalous-material-detail-side';
text(side, 'h2', t('materialImageDetail'));
const loading = text(side, 'p', t('materialInspecting'), 'anomalous-material-muted');
dialog.append(media, side);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
overlay.addEventListener('click', event => { if (event.target === overlay) overlay.remove(); });
try {
const response = await fetch('/anomalous/inspect_image_material', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source_image: sourceImage }),
});
const payload = await jsonResponse(response, 'material inspection failed');
if (payload.status !== 'success') throw new Error(payload.message || 'material inspection failed');
loading.remove();
text(side, 'p', t('materialSnapshotExplanation'), 'anomalous-material-muted');
const stats = document.createElement('div');
stats.className = 'anomalous-material-detail-stats';
text(stats, 'span', t('materialNodeSummary', { count: payload.node_count || 0 }));
text(stats, 'span', t('materialModelSummary', { count: payload.model_references?.length || 0 }));
side.appendChild(stats);
if (payload.prompt_excerpt) text(side, 'blockquote', payload.prompt_excerpt, 'anomalous-material-prompt-excerpt');
const label = text(side, 'label', t('materialName'));
const name = document.createElement('input');
name.type = 'text';
name.maxLength = 120;
name.value = payload.suggested_name || '';
label.appendChild(name);
const hint = text(side, 'small', t('materialSaveHint'), 'anomalous-material-muted');
hint.style.marginTop = 'auto';
const save = text(side, 'button', `💾 ${t('materialSaveSnapshot')}`, 'anomalous-btn-primary');
save.type = 'button';
save.onclick = async () => {
const value = name.value.trim();
if (!value) { name.focus(); return; }
save.disabled = true;
try {
const saveResponse = await fetch('/anomalous/save_image_material', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source_image: sourceImage, name: value }),
});
const savePayload = await jsonResponse(saveResponse, 'material save failed');
if (savePayload.status !== 'success') throw new Error(savePayload.message || 'material save failed');
owner?.refreshMaterials?.();
overlay.remove();
await anomalousAlert(t('materialSaveSuccess'));
} catch (error) {
console.error('Could not save image material:', error);
await anomalousAlert(t('materialSaveError'));
save.disabled = false;
}
};
name.focus();
name.select();
} catch (error) {
console.error('Could not inspect image material:', error);
loading.textContent = t('materialInspectError');
}
}