import { $el, ComfyDialog } from '../../scripts/ui.js' import { manager_instance, rebootAPI, fetchData, md5, icons } from './common.js' // https://cenfun.github.io/turbogrid/api.html import TG from './turbogrid.esm.js' const pageCss = ` .cmm-manager { --grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; z-index: 10001; width: 80%; height: 80%; display: flex; flex-direction: column; gap: 10px; color: var(--fg-color); font-family: arial, sans-serif; } .cmm-manager .cmm-flex-auto { flex: auto; } .cmm-manager button { font-size: 16px; color: var(--input-text); background-color: var(--comfy-input-bg); border-radius: 8px; border-color: var(--border-color); border-style: solid; margin: 0; padding: 4px 8px; min-width: 100px; } .cmm-manager button:disabled, .cmm-manager input:disabled, .cmm-manager select:disabled { color: gray; } .cmm-manager button:disabled { background-color: var(--comfy-input-bg); } .cmm-manager-header { display: flex; flex-wrap: wrap; gap: 5px; justify-content: space-between; align-items: center; padding: 0 5px; } .cmm-manager-header label { display: flex; flex: 1; gap: 10px; align-items: center; } .cmm-manager-header label[required="true"] span::before { display: inline-block; margin-inline-end: 4px; color: #ff4d4f; font-family: SimSun, sans-serif; line-height: 1; content: "*"; } .cmm-manager-model_id, .cmm-manager-revision, .cmm-manager-filename, .cmm-manager-save_path { flex: 1; min-width: 100px; height: 28px; line-height: 28px; } .cmm-manager-keywords { height: 28px; line-height: 28px; padding: 0 5px 0 26px; background-size: 16px; background-position: 5px center; background-repeat: no-repeat; background-image: url("data:image/svg+xml;charset=utf8,${encodeURIComponent( icons.search.replace('currentColor', '#888') )}"); } .cmm-manager-help a { font-size: 14px; color: skyblue; text-decoration: none; } .cmm-manager-grid { flex: auto; border: 1px solid var(--border-color); overflow: hidden; } .cmm-manager-message { } .cmm-manager-footer { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } .cmm-manager-grid .tg-turbogrid { font-family: var(--grid-font); font-size: 15px; background: var(--bg-color); } .cmm-manager-grid .cmm-node-name a { color: skyblue; text-decoration: none; word-break: break-word; } .cmm-manager-grid .cmm-node-desc a { color: #5555FF; font-weight: bold; text-decoration: none; } .cmm-manager-grid .tg-cell a:hover { text-decoration: underline; } .cmm-icon-passed { width: 20px; height: 20px; position: absolute; left: calc(50% - 10px); top: calc(50% - 10px); } .cmm-status-error-container { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; } .cmm-status-error-container .cmm-icon-conflicts { color: #ff4d4f; position: relative; width: 20px; height: 20px; margin-right: 4px; } .cmm-status-error-container button { padding: 2px; font-size: 14px; min-width: 72px; } .cmm-manager .cmm-btn-enable { background-color: blue; color: white; } .cmm-manager .cmm-btn-disable { background-color: MediumSlateBlue; color: white; } .cmm-manager .cmm-btn-install { background-color: black; color: white; } .cmm-btn-download { width: 18px; height: 18px; position: absolute; left: calc(50% - 10px); top: calc(50% - 10px); cursor: pointer; opacity: 0.8; color: #fff; } .cmm-btn-download:hover { opacity: 1; } .cmm-manager-light .cmm-btn-download { color: #000; } @keyframes cmm-btn-loading-bg { 0% { left: 0; } 100% { left: -105px; } } .cmm-manager button.cmm-btn-loading { position: relative; overflow: hidden; border-color: rgb(0 119 207 / 80%); background-color: var(--comfy-input-bg); } .cmm-manager button.cmm-btn-loading::after { position: absolute; top: 0; left: 0; content: ""; width: 500px; height: 100%; background-image: repeating-linear-gradient( -45deg, rgb(0 119 207 / 30%), rgb(0 119 207 / 30%) 10px, transparent 10px, transparent 15px ); animation: cmm-btn-loading-bg 2s linear infinite; } .cmm-manager-light .cmm-node-name a { color: blue; } .cmm-manager-light .cm-warn-note { background-color: #ccc !important; } .cmm-manager-light .cmm-btn-install { background-color: #333; } .cmm-progress-bar-container { height: 100%; display: flex; align-items: center; } .cmm-progress-bar-container > span { font-size: 14px; color: var(--descrip-text); } .cmm-progress-bar { position: relative; display: inline-block; height: 8px; margin-right: 4px; border-radius: 4px; overflow: hidden; background-color: #888888; } .cmm-progress-completed { background-color: #90EE90; height: 100%; position: absolute; top: 0; left: 0; } .cmm-tooltip { position: absolute; z-index: 1000; max-width: 300px; background-color: var(--comfy-input-bg); color: var(--input-text); padding: 6px 10px; border-radius: 4px; border-color: var(--border-color); border-style: solid; border-width: 2px; font-size: 14px; opacity: 0; visibility: hidden; word-wrap: break-word; } .cmm-tooltip-message { position: relative; z-index: 2; } .cmm-tooltip-message::after { content: ""; position: absolute; z-index: -1; left: -10px; right: -10px; top: -6px; bottom: -6px; width: calc(100% + 20px); height: calc(100% + 12px); background-color: var(--comfy-input-bg); } .cmm-tooltip.cmm-tooltip-visible { opacity: 1; visibility: visible; } .cmm-tooltip::before { content: ""; position: absolute; z-index: 1; top: 100%; width: 8px; height: 8px; left: 50%; background-color: var(--comfy-input-bg); transform: translate(-50%, -6.6px) rotate(45deg); border-color: var(--border-color); border-style: solid; border-width: 2px; } ` const pageHtml = `
Documentation
` export class ModelManager { static instance = null constructor(app, manager_dialog) { this.app = app this.locale = localStorage.getItem('AGL.Locale') || 'en-US' this.manager_dialog = manager_dialog this.id = 'cmm-manager' this.form_data = { model_id: '', revision: '', filename: '', save_path: '' } this.keywords = '' this.init() } init() { // 添加样式 if (!document.querySelector(`style[context="${this.id}"]`)) { const $style = document.createElement('style') $style.setAttribute('context', this.id) $style.innerHTML = pageCss document.head.appendChild($style) } // 添加 modal this.element = $el('div', { parent: document.body, className: 'comfy-modal cmm-manager' }) this.element.innerHTML = pageHtml this.bindEvents() this.initGrid() } getTranslation(key) { const translationMap = { 'en-US': { model_id: 'Model ID', filename: 'Filename', save_path: 'Save Path', status: 'Status', no_results: 'No Results', retry: 'Retry' }, 'zh-CN': { model_id: '模型 ID', filename: '文件名', save_path: '保存路径', status: '状态', no_results: '暂无结果', retry: '重试' } } if (!translationMap[this.locale]) { return translationMap['en-US'][key] } return translationMap[this.locale][key] || translationMap['en-US'][key] } bindEvents() { const eventsMap = { '.cmm-manager-model_id': { change: (e) => { this.form_data.model_id = e.target.value } }, '.cmm-manager-revision': { change: (e) => { this.form_data.revision = e.target.value } }, '.cmm-manager-filename': { change: (e) => { this.form_data.filename = e.target.value } }, '.cmm-manager-save_path': { change: (e) => { this.form_data.save_path = e.target.value } }, '.cmm-btn-install': { click: async (e) => { if (this.installing) { return } this.installing = true const res = await this.installModel(this.form_data, e.currentTarget) this.installing = false if (res) { this.setFormData() await this.loadData(true) } } }, '.cmm-manager-keywords': { input: (e) => { const keywords = `${e.target.value}`.trim() if (keywords !== this.keywords) { this.keywords = keywords this.updateGrid() } }, focus: (e) => e.target.select() }, '.cmm-manager-close': { click: (e) => this.close() } } Object.keys(eventsMap).forEach((selector) => { const target = this.element.querySelector(selector) if (target) { const events = eventsMap[selector] if (events) { Object.keys(events).forEach((type) => { target.addEventListener(type, events[type]) }) } } }) } // =========================================================================================== initGrid() { const container = this.element.querySelector('.cmm-manager-grid') const grid = new TG.Grid(container) this.grid = grid // 绑定事件监听 grid.bind('onClick', async (e, d) => { const { rowItem } = d const target = d.e.target const mode = target.getAttribute('mode') if (mode === 'retry') { const res = await this.installModel( { model_id: rowItem.model_id, revision: rowItem.revision, filename: rowItem.filename, save_path: rowItem.save_path }, target ) if (res) { await this.loadData(true) } } }) const isNodeTruncated = function (node) { if (!node) { return false } if (node.clientWidth < node.scrollWidth) { return true } } grid .bind('onMouseOver', (e, d) => { const { rowItem } = d const target = d.e.target const mode = target.getAttribute('mode') if (mode === 'model-error-message') { this.showTooltip(target, rowItem.message) return } const value = d.rowItem[d.columnItem.id] if (value) { if (isNodeTruncated(target)) { this.showTooltip(target, `${value}`) } else if (isNodeTruncated(d.e.relatedTarget)) { this.showTooltip(d.e.relatedTarget, `${value}`) } return } }) .bind('onMouseOut', (e, d) => { this.hideTooltip() }) grid.setOption({ theme: 'dark', selectVisible: false, selectMultiple: true, selectAllVisible: true, textSelectable: true, scrollbarRound: true, frozenColumn: 1, rowNotFound: this.getTranslation('no_results'), rowHeight: 40, bindWindowResize: true, bindContainerResize: true, cellResizeObserver: (rowItem, columnItem) => { const autoHeightColumns = ['name', 'description'] return autoHeightColumns.includes(columnItem.id) }, // updateGrid handler for filter and keywords rowFilter: (rowItem) => { const searchableColumns = [ 'model_id', 'revision', 'save_path', 'filename' ] let shouldShown = grid.highlightKeywordsFilter( rowItem, searchableColumns, this.keywords ) return shouldShown } }) } renderGrid() { // update theme const colorPalette = this.app.ui.settings.settingsValues['Comfy.ColorPalette'] Array.from(this.element.classList).forEach((cn) => { if (cn.startsWith('cmm-manager-')) { this.element.classList.remove(cn) } }) this.element.classList.add(`cmm-manager-${colorPalette}`) const options = { theme: colorPalette === 'light' ? '' : 'dark' } const rows = this.modelList || [] const columns = [ { id: 'model_id', name: this.getTranslation('model_id'), minWidth: 100, width: 300, maxWidth: 500, classMap: 'cmm-node-name', formatter: function (id, rowItem, columnItem, cellNode) { return `${id}` } }, { id: 'status', name: this.getTranslation('status'), width: 130, minWidth: 110, width: 200, maxWidth: 500, sortable: false, align: 'center', formatter: (status, rowItem, columnItem) => { switch (status) { case 0: case 1: return `
${rowItem.progress}%
${icons.conflicts}
` case 2: return `
${icons.passed}
` default: return '' } } }, { id: 'revision', name: 'Revision', width: 150, align: 'center' }, { id: 'filename', name: this.getTranslation('filename'), width: 300 }, { id: 'save_path', name: this.getTranslation('save_path'), width: 300 } ] this.grid.setData({ options, rows, columns }) this.grid.render() } updateGrid() { if (this.grid) { this.grid.update() } } // =========================================================================================== async getModelsList() { if (this.fetching) { return } this.fetching = true const res = await fetchData('/customnode/modelscope/status') this.fetching = false return res } async loadData(update, loop) { !loop && this.showLoading() const res = await this.getModelsList() if (!res) { !loop && this.hideLoading() return } if (res.error) { this.showError('Failed to get external model list.') !loop && this.hideLoading() return } const models = res.data this.modelList = models if (update) { this.grid.setRows(this.modelList) this.updateGrid() } else { this.renderGrid() } if (models.some((item) => item.status === 1 || item.status === 0)) { setTimeout(() => { this.loadData(update, true) }, 500) } !loop && this.hideLoading() } async installModel(data, btn) { const prop2Label = { model_id: 'Model ID', save_path: 'Save Path', filename: 'Filename' } for (const key in prop2Label) { if (!data[key]) { this.showError(`${prop2Label[key]} is required.`) return false } } btn.classList.add('cmm-btn-loading') this.showLoading() this.showError('') const res = await fetchData('/customnode/modelscope/install_model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) if (res.error) { this.showError(`Install failed: ${data.model_id} ${res.error.message}`) } this.hideLoading() btn.classList.remove('cmm-btn-loading') return true } // =========================================================================================== showTooltip(target, message) { if (!message) { this.hideTooltip() return } const containerRect = this.element.getBoundingClientRect() const tooltipElement = document.querySelector('.cmm-tooltip') tooltipElement.innerHTML = `
${message}
` // 计算工具提示的位置 const targetRect = target.getBoundingClientRect() const tooltipHeight = tooltipElement.offsetHeight const tooltipWidth = tooltipElement.offsetWidth tooltipElement.style.top = `${ targetRect.top - containerRect.top - tooltipHeight - 10 }px` // 在目标元素上方 tooltipElement.style.left = `${ targetRect.left - containerRect.left + (targetRect.width - tooltipWidth) / 2 }px` // 居中对齐 tooltipElement.classList.add('cmm-tooltip-visible') } hideTooltip() { const tooltipElement = document.querySelector('.cmm-tooltip') tooltipElement.classList.remove('cmm-tooltip-visible') } showError(err) { this.showMessage(err, 'red') } showMessage(msg, color) { if (color) { msg = `${msg}` } this.element.querySelector('.cmm-manager-message').innerHTML = msg } showLoading() { this.setDisabled(true) if (this.grid) { this.grid.showLoading() this.grid.showMask({ opacity: 0.05 }) } } hideLoading() { this.setDisabled(false) if (this.grid) { this.grid.hideLoading() this.grid.hideMask() } } setDisabled(disabled) { const $close = this.element.querySelector('.cmm-manager-close') const list = [ '.cmm-manager-header input,button', '.cmm-manager-header select', '.cmm-manager-footer button' ] .map((s) => { return Array.from(this.element.querySelectorAll(s)) }) .flat() .filter((it) => { return it !== $close }) list.forEach(($elem) => { if (disabled) { $elem.setAttribute('disabled', 'disabled') } else { $elem.removeAttribute('disabled') } }) Array.from(this.element.querySelectorAll('.cmm-btn-loading')).forEach( ($elem) => { $elem.classList.remove('cmm-btn-loading') } ) } setKeywords(keywords = '') { this.keywords = keywords this.element.querySelector('.cmm-manager-keywords').value = keywords } setFormData( data = { model_id: '', revision: '', filename: '', save_path: '' } ) { this.form_data = data Object.keys(data).map((key) => { this.element.querySelector(`.cmm-manager-${key}`).value = data[key] }) } show() { this.element.style.display = 'flex' this.setKeywords('') this.showMessage('') this.loadData() } close() { this.element.style.display = 'none' } }