Files
Will Miao ededac14d3 feat(settings): add About section with environment details (#1194)
Move the version panel out of Help -> Documentation into a dedicated
Settings -> About section. It shows the LoRA Manager, ComfyUI, ComfyUI
Frontend, Python, OS and browser versions, copies the block as Markdown
for issue reports, and links to the issue tracker, wiki, GitHub and
Discord.

Read the values from a new GET /api/lm/environment endpoint instead of
ComfyUI's /system_stats: Python and OS stay available in standalone mode,
while ComfyUI-only versions are omitted there instead of failing the
whole panel. Model Paths is inserted before About so About remains the
last settings section in standalone mode as well.
2026-11-09 12:24:31 +08:00

190 lines
5.9 KiB
JavaScript

import { showToast } from '../utils/uiHelpers.js';
import { translate } from '../utils/i18nHelpers.js';
/**
* Renders the read-only environment block in Settings -> About and copies it
* as Markdown for issue reports.
*/
export class EnvironmentPanel {
constructor() {
this.panel = null;
this.copyButton = null;
this.data = null;
this.promise = null;
this.initialized = false;
}
initialize() {
if (this.initialized) {
return;
}
this.panel = document.getElementById('settingsEnvironmentPanel');
if (!this.panel) {
return;
}
this.copyButton = document.getElementById('copyEnvironmentBtn');
if (this.copyButton) {
this.copyButton.addEventListener('click', () => this.copyEnvironmentInfo());
}
this.initialized = true;
}
/**
* Load the environment snapshot once, on first visit to the About section.
*
* The backend reports Python/OS in both plugin and standalone mode, and
* ComfyUI package versions when they are installed; missing values are
* rendered as "Unavailable".
*
* @returns {Promise<Object|null>} The rendered environment snapshot
*/
ensureLoaded() {
this.initialize();
if (!this.panel) {
return Promise.resolve(null);
}
if (this.data) {
return Promise.resolve(this.data);
}
if (this.promise) {
return this.promise;
}
const base = this.collectBaseEnvironment();
this.render(base, { pending: true });
this.promise = this.load(base);
return this.promise;
}
async load(base) {
let data = base;
try {
const response = await fetch('/api/lm/environment', {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const payload = await response.json();
const environment = payload?.environment || {};
data = {
...base,
app: environment.app_version || base.app,
comfyui: environment.comfyui_version || '',
frontend: environment.frontend_version || '',
python: environment.python_version || '',
os: environment.platform || '',
};
} catch (error) {
console.debug('Environment info unavailable:', error);
}
this.data = data;
this.render(data);
return data;
}
/**
* Collect the environment values that are available without a fetch.
*/
collectBaseEnvironment() {
return {
app: document.body?.dataset?.appVersion || '',
browser: navigator.userAgent || '',
};
}
/**
* Render an environment snapshot into the panel fields.
* @param {Object} data - Field values keyed by data-env-field
* @param {{pending?: boolean}} [options] - Keep placeholders while loading
*/
render(data, { pending = false } = {}) {
if (!this.panel) {
return;
}
const fallback = pending
? '\u2014'
: translate('settings.about.unavailable', {}, 'Unavailable');
this.panel.querySelectorAll('[data-env-field]').forEach((element) => {
const value = data?.[element.dataset.envField] || '';
element.textContent = value || fallback;
element.title = value;
});
}
/**
* Copy the environment panel as Markdown for pasting into an issue.
*
* The copied block stays in English so issue reports remain readable for
* maintainers regardless of the reporter's UI language.
*/
async copyEnvironmentInfo() {
const data = this.data || (await this.ensureLoaded()) || this.collectBaseEnvironment();
const labels = {
app: 'LoRA Manager',
comfyui: 'ComfyUI',
frontend: 'ComfyUI Frontend',
python: 'Python',
os: 'OS',
browser: 'Browser',
};
const unavailable = 'Unavailable';
const lines = ['**Environment**'];
Object.entries(labels).forEach(([key, label]) => {
lines.push(`- ${label}: ${data[key] || unavailable}`);
});
const copied = await this.writeToClipboard(lines.join('\n'));
showToast(
copied ? 'settings.about.copySuccess' : 'settings.about.copyFailed',
{},
copied ? 'success' : 'error'
);
}
/**
* Write text to the clipboard, falling back to execCommand on insecure
* origins where navigator.clipboard is unavailable (LAN HTTP deployments).
*/
async writeToClipboard(text) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (error) {
console.debug('Clipboard API write failed, using fallback:', error);
}
}
return this.writeToClipboardFallback(text);
}
/**
* Legacy clipboard fallback for non-secure contexts.
*/
writeToClipboardFallback(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
let copied = false;
try {
copied = typeof document.execCommand === 'function' && document.execCommand('copy');
} catch (error) {
copied = false;
} finally {
textarea.remove();
}
return copied;
}
}
export const environmentPanel = new EnvironmentPanel();