import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; const spinner = ` ` const style = ` .cpack-tree-list { max-height: 300px; overflow-y: auto; border: 1px solid #444; border-radius: 4px; padding: 5px; } .cpack-tree-item { padding: 3px 0; } .cpack-tree-item label { display: flex; align-items: center; gap: 5px; min-height: 20px; } .cpack-tree-children { margin-left: 12px; } .cpack-tree-toggle { width: 12px; height: 12px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; color: #888; font-size: 0.8em; min-width: 12px; } .cpack-tree-toggle:hover { color: #fff; } .cpack-tree-toggle.empty { visibility: hidden; } .cpack-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1a1a1a; padding: 20px; border-radius: 8px; z-index: 1000; color: white; min-width: 360px; } .cpack-input { width: 100%; padding: 8px; background: #333; border: 1px solid #444; border-radius: 4px; color: white; box-sizing: border-box; } .cpack-btn { padding: 6px 12px; background: #666; border: none; border-radius: 4px; color: white; cursor: pointer; } .cpack-btn.primary { background: #00a67d; } .cpack-btn.primary:disabled { background: #81a39b; cursor: not-allowed; } .cpack-btn-container { margin-top: 20px; display: flex; justify-content: flex-end; gap: 10px; } .cpack-title { margin-bottom: 15px; font-size: 1.3em; font-weight: bold; } .cpack-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 999; } .cpack-input-row { display: flex; align-items: center; gap: 5px; margin-bottom: 5px; } .cpack-form-item { margin-bottom: 15px; } .cpack-form-item:last-child { margin-bottom: -5px; } .cpack-form-item label { margin-bottom: 5px; } .cpack-form-item.required label::after { content: "*"; color: #ff8383; } #build-info { display: flex; padding: 10px; flex-direction: column; align-items: center; } #build-error { display: none; color: #ff8383; text-wrap: auto; overflow-x: auto; background: #333; padding: 10px; } .cpack-copyable { display: flex; justify-content: center; align-items: center; gap: 5px; } .cpack-copyable > span { border: 1px solid #ccc; padding: 3px 10px; border-radius: 3px; background: #606060; } .error-message { color: #ff8383; margin-top: 5px; display: none; font-size: 0.9em; } ` class TreeState { constructor() { this.selectedFiles = new Set(); this.subscribers = new Set(); } subscribe(callback) { this.subscribers.add(callback); return () => this.subscribers.delete(callback); } notify() { this.subscribers.forEach(callback => callback(this.selectedFiles)); } toggle(path, checked) { if (checked) { this.selectedFiles.add(path); } else { this.selectedFiles.delete(path); } this.notify(); } toggleMultiple(paths, checked) { paths.forEach(path => { if (checked) { this.selectedFiles.add(path); } else { this.selectedFiles.delete(path); } }); this.notify(); } isSelected(path) { return this.selectedFiles.has(path); } clear() { this.selectedFiles.clear(); this.notify(); } getSelectedCount() { return this.selectedFiles.size; } getSelectedFiles() { return Array.from(this.selectedFiles); } } class FileTreeList { constructor(container, countId) { this.container = container; this.countId = countId; this.state = new TreeState(); this.init(); } init() { this.container.classList.add('cpack-tree-list'); this.state.subscribe(() => this.updateCount()); } async load() { try { const { workflow, output: workflow_api } = await app.graphToPrompt(); const files = await this.getInputFiles(workflow, workflow_api); // 预先将默认选中的文件添加到选中列表 files.forEach(file => { const path = file.path || file; if (file.checked) { this.state.toggle(path, true); } }); this.renderTree(this.buildTree(files)); // 更新所有父目录的状态 this.container.querySelectorAll("[data-action='check-dir']").forEach(checkbox => { this.updateFolderState(checkbox); }); // 更新总数 this.updateCount(); } catch(e) { this.container.innerHTML = `
Failed to load files: ${e.message}
`; } } async getInputFiles(workflow, workflow_api) { const resp = await api.fetchApi("/bentoml/file/query", { method: "POST", body: JSON.stringify({ workflow, workflow_api }), headers: { "Content-Type": "application/json" } }); const data = await resp.json(); return Array.isArray(data) ? data : (data.files || []); } buildTree(files) { const root = { name: 'root', children: {}, files: [] }; for (const file of files) { const filePath = file.path || file; const parts = filePath.split(/[\/\\]/); let current = root; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (i === parts.length - 1) { // 这是文件 current.files.push({ name: part, path: file.path || file, badges: file.badges || [], checked: file.checked || false }); // 对文件按名称排序 current.files.sort((a, b) => a.name.localeCompare(b.name)); } else { // 这是目录 if (!current.children[part]) { current.children[part] = { name: part, children: {}, files: [] }; } current = current.children[part]; } } } // 对目录按名称排序 for (const dir in root.children) { root.children[dir].files.sort((a, b) => a.name.localeCompare(b.name)); } return root; } renderTree(node, level = 0) { const dirs = Object.values(node.children); const hasChildren = dirs.length > 0 || node.files.length > 0; this.container.innerHTML = this.renderNode(node, true); this.setupEventListeners(); this.updateCount(); } renderNode(node, isRoot = false) { const dirs = Object.values(node.children); const hasChildren = dirs.length > 0 || node.files.length > 0; let html = ''; if (!isRoot) { html += `
`; } if (hasChildren) { html += `
`; // 渲染子目录 for (const dir of dirs) { html += this.renderNode(dir); } // 渲染文件 for (const file of node.files) { html += `
`; } html += '
'; } return html; } updateFolderState(folderCheckbox) { const treeItem = folderCheckbox.closest('.cpack-tree-item'); const children = treeItem.nextElementSibling; if (!children) return; const childFiles = children.querySelectorAll("input[name='files']"); const checkedCount = Array.from(childFiles).filter(cb => cb.checked).length; if (checkedCount === 0) { folderCheckbox.checked = false; folderCheckbox.indeterminate = false; } else if (checkedCount === childFiles.length) { folderCheckbox.checked = true; folderCheckbox.indeterminate = false; } else { folderCheckbox.checked = false; folderCheckbox.indeterminate = true; } // 递归更新父文件夹状态 const parentFolder = treeItem.parentElement.closest('.cpack-tree-item'); if (parentFolder) { const parentCheckbox = parentFolder.querySelector("[data-action='check-dir']"); if (parentCheckbox) { this.updateFolderState(parentCheckbox); } } // 更新选中文件总数 this.updateCount(); } setupEventListeners() { // 目录选择功能 this.container.querySelectorAll("[data-action='check-dir']").forEach(checkbox => { checkbox.addEventListener('change', (e) => { const treeItem = e.target.closest('.cpack-tree-item'); const children = treeItem.nextElementSibling; if (children) { const childFiles = Array.from(children.querySelectorAll("input[name='files']")) .map(input => input.value); this.state.toggleMultiple(childFiles, e.target.checked); // 更新UI children.querySelectorAll("input[type='checkbox']").forEach(child => { child.checked = e.target.checked; if (child.hasAttribute('data-action')) { child.indeterminate = false; } }); } }); }); // 折叠功能 this.container.querySelectorAll("[data-action='toggle']").forEach(toggle => { toggle.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const treeItem = e.target.closest('.cpack-tree-item'); const children = treeItem.nextElementSibling; if (children) { children.style.display = children.style.display === 'none' ? '' : 'none'; e.target.textContent = children.style.display === 'none' ? '▶' : '▼'; } }); }); // 文件选择功能 this.container.querySelectorAll("input[name='files']").forEach(checkbox => { checkbox.addEventListener('change', (e) => { this.state.toggle(e.target.value, e.target.checked); // 更新父文件夹状态 const parentFolder = checkbox.closest('.cpack-tree-children') ?.previousElementSibling ?.querySelector("[data-action='check-dir']"); if (parentFolder) { this.updateFolderState(parentFolder); } }); }); } updateCount() { const countSpan = document.querySelector(`[data-files-count='${this.countId}']`); if (countSpan) { countSpan.textContent = this.state.getSelectedCount(); } } getSelectedFiles() { return this.state.getSelectedFiles(); } } class ModelList { constructor(container, countId) { this.container = container; this.countId = countId; this.selectedModels = new Set(); this.init(); } init() { this.container.style.cssText = ` max-height: 300px; overflow-y: auto; border: 1px solid #444; border-radius: 4px; padding: 5px; `; } async load() { try { const { workflow, output: workflow_api } = await app.graphToPrompt(); const resp = await api.fetchApi("/bentoml/model/query", { method: "POST", body: JSON.stringify({ workflow, workflow_api }), headers: { "Content-Type": "application/json" } }); const data = await resp.json(); const models = Array.isArray(data) ? data : (data.models || []); this.renderModels(this.sortModels(models)); } catch(e) { this.container.innerHTML = `
Failed to load models: ${e.message}
`; } } sortModels(models) { return models.sort((a, b) => { if (a.refered && !b.refered) return -1; if (!a.refered && b.refered) return 1; const timeA = Math.max(a.atime || 0, a.ctime || 0); const timeB = Math.max(b.atime || 0, b.ctime || 0); return timeB - timeA; }); } renderModels(models) { const now = Date.now() / 1000; const ONE_DAY = 24 * 60 * 60; this.container.innerHTML = this.getSelectAllHtml() + models.map(model => this.getModelItemHtml(model, now, ONE_DAY)).join(''); this.setupEventListeners(); this.updateCount(); } getSelectAllHtml() { return `
`; } getModelItemHtml(model, now, ONE_DAY) { const size = (model.size / (1024 * 1024)).toFixed(2); const path = String(model.filename || ''); const name = path ? path.split('/').pop() : 'Unknown'; const isRecentlyAccessed = (now - (model.atime || 0)) < ONE_DAY; return `
`; } setupEventListeners() { this.container.querySelectorAll("input[name='models']").forEach(checkbox => { checkbox.addEventListener('change', () => this.updateCount()); }); const selectAllCheckbox = this.container.querySelector("#select-all-models"); selectAllCheckbox?.addEventListener('change', (e) => { this.container.querySelectorAll("input[name='models']").forEach(checkbox => { checkbox.checked = e.target.checked; }); this.updateCount(); }); } updateCount() { const count = this.container.querySelectorAll("input[name='models']:checked").length; const countSpan = document.querySelector(`[data-models-count='${this.countId}']`); if (countSpan) { countSpan.textContent = count; } } getSelectedModels() { // 只返回用户选中的模型 return Array.from(this.container.querySelectorAll("input[name='models']:checked")) .map(input => input.value); } } function createModal(modal) { const overlay = document.createElement("div"); overlay.className = "cpack-overlay"; document.body.appendChild(overlay); document.body.appendChild(modal); return { close: () => { modal.remove(); overlay.remove(); } }; } async function createPackModal() { return new Promise((resolve) => { const modal = document.createElement("div"); modal.className = "cpack-modal"; modal.id = "input-modal"; const title = document.createElement("div"); title.textContent = "Package Workflow"; title.className = "cpack-title"; const form = document.createElement("form"); form.innerHTML = `
`; const buttonContainer = document.createElement("div"); buttonContainer.className = "cpack-btn-container"; const confirmButton = document.createElement("button"); confirmButton.textContent = "Pack"; confirmButton.className = "cpack-btn primary"; confirmButton.disabled = true; const cancelButton = document.createElement("button"); cancelButton.textContent = "Cancel"; cancelButton.className = "cpack-btn"; buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(confirmButton); modal.appendChild(title); modal.appendChild(form); modal.appendChild(buttonContainer); const { close } = createModal(modal); const packageOptionsContainer = form.querySelector("#package-options-container"); const packageOptions = new PackageOptions(form, "pack-models-list", "pack-files-list", true); packageOptionsContainer.innerHTML = packageOptions.getHtml(); packageOptions.init().then(() => { confirmButton.disabled = false; }); confirmButton.onclick = () => { const filename = form.querySelector("input[name='filename']").value.trim(); if (filename) { // Save filename to localStorage localStorage.setItem('cpack-bento-name', filename); const selectedData = packageOptions.getSelectedData(); close(); resolve({ filename, models: selectedData.models, files: selectedData.files, systemPackages: selectedData.systemPackages }); } }; cancelButton.onclick = () => { close(); resolve(null); }; const filenameInput = form.querySelector("input[name='filename']"); filenameInput.addEventListener("keyup", (e) => { if (e.key === "Enter") { confirmButton.click(); } }); filenameInput.select(); }); } function createDownloadModal() { const modal = document.createElement("div"); modal.className = "cpack-modal"; modal.id = "download-modal"; const title = document.createElement("div"); title.textContent = "Packaging..."; title.style.marginBottom = "15px"; title.style.color = "#fff"; const hint = document.createElement("div"); hint.textContent = "First run may take a few minutes to compute model checksums, depending on the number of models."; hint.style.cssText = "color: #888; font-size: 0.9em; margin-bottom: 15px;"; const progress = document.createElement("div"); progress.style.cssText = ` width: 100%; height: 20px; background: #333; border-radius: 10px; overflow: hidden; `; const progressBar = document.createElement("div"); progressBar.style.cssText = ` width: 0%; height: 100%; background: #00a67d; transition: width 0.3s ease; `; progress.appendChild(progressBar); modal.appendChild(title); modal.appendChild(hint); modal.appendChild(progress); const { close } = createModal(modal); return { updateProgress: (percent) => { progressBar.style.width = `${percent}%`; }, close }; } async function unpackAction() { const modal = document.createElement("div"); modal.className = "cpack-modal"; modal.style.width = "500px"; const title = document.createElement("div"); title.textContent = "Unpack Instructions"; title.className = "cpack-title"; const content = document.createElement("div"); content.innerHTML = `

To unpack a workflow package, follow these steps:

1. Install comfy-pack CLI:

pip install comfy-pack

2. Run the unpack command:

comfy-pack unpack <your-app>.cpack.zip
`; const buttonContainer = document.createElement("div"); buttonContainer.className = "cpack-btn-container"; const closeButton = document.createElement("button"); closeButton.textContent = "Close"; closeButton.className = "cpack-btn"; buttonContainer.appendChild(closeButton); modal.appendChild(title); modal.appendChild(content); modal.appendChild(buttonContainer); const { close } = createModal(modal); closeButton.onclick = close; } async function packageAction() { if (document.getElementById("input-modal")) return; if (document.getElementById("download-modal")) return; const result = await createPackModal(); if (!result) return; const downloadModal = createDownloadModal(); try { downloadModal.updateProgress(20); const { workflow, output: workflow_api } = await app.graphToPrompt(); downloadModal.updateProgress(40); const body = JSON.stringify({ workflow, workflow_api, models: result.models, files: result.files, system_packages: result.systemPackages }); downloadModal.updateProgress(60); const resp = await api.fetchApi("/bentoml/pack", { method: "POST", body, headers: { "Content-Type": "application/json" } }); downloadModal.updateProgress(80); const downloadUrl = (await resp.json())["download_url"]; downloadModal.updateProgress(100); const link = document.createElement("a"); link.href = downloadUrl; link.download = result.filename + ".cpack.zip"; link.click(); setTimeout(() => { downloadModal.close(); }, 1000); } catch (error) { console.error("Package failed:", error); downloadModal.close(); } } async function serveAction() { if (document.getElementById("serve-modal")) return; const modal = document.createElement("div"); modal.id = "serve-modal"; modal.className = "cpack-modal"; modal.style.width = "400px"; const title = document.createElement("div"); title.textContent = "Serve Workflow as REST API"; title.className = "cpack-title"; const form = document.createElement("form"); form.innerHTML = serveForm; const buttonContainer = document.createElement("div"); buttonContainer.className = "cpack-btn-container"; const confirmButton = document.createElement("button"); confirmButton.textContent = "Start"; confirmButton.className = "cpack-btn primary"; const cancelButton = document.createElement("button"); cancelButton.textContent = "Cancel"; cancelButton.className = "cpack-btn"; buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(confirmButton); modal.appendChild(title); modal.appendChild(form); modal.appendChild(buttonContainer); const { close } = createModal(modal); cancelButton.onclick = close; form.querySelector("input[name='port']").select(); confirmButton.onclick = async (e) => { e.preventDefault(); const formData = new FormData(form); const { workflow, output: workflow_api } = await app.graphToPrompt(); const data = { host: formData.get("allowExternal") === "on" ? "0.0.0.0" : "localhost", port: formData.get("port"), workflow, workflow_api }; try { confirmButton.disabled = true; const resp = await api.fetchApi("/bentoml/serve", { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" } }); const result = await resp.json(); if (result.error) { const errorDiv = form.querySelector('.error-message'); errorDiv.textContent = result.error; errorDiv.style.display = 'block'; } else { close(); createServeStatusModal(result.url); } } catch(e) { const errorDiv = form.querySelector('.error-message'); errorDiv.textContent = e.message; errorDiv.style.display = 'block'; } finally { confirmButton.disabled = false; } }; } async function deployAction() { if (document.getElementById("build-modal")) return; if (document.getElementById("building-modal")) return; const data = await createBuildModal(); console.log(data); await createBuildingModal(data); }; const serveForm = `
` class PackageOptions { constructor(container, modelsListId, filesListId, defaultOpen = true) { this.container = container; this.modelsListId = modelsListId; this.filesListId = filesListId; this.modelListComponent = null; this.fileListComponent = null; this.defaultOpen = defaultOpen; } getHtml() { return `
Package Options
Models (0 selected)
${spinner}
Input Files (0 selected)
${spinner}
System Packages
`; } async init() { const modelsList = this.container.querySelector(`#${this.modelsListId}`); const filesList = this.container.querySelector(`#${this.filesListId}`); this.modelListComponent = new ModelList(modelsList, this.modelsListId); this.fileListComponent = new FileTreeList(filesList, this.filesListId); const addButton = this.container.querySelector("#add-button"); const systemPackagesArray = this.container.querySelector("#system-packages-array"); addButton.addEventListener("click", (e) => { e.preventDefault(); const row = document.createElement("div"); row.className = "cpack-input-row"; row.innerHTML = `
` systemPackagesArray.appendChild(row); row.querySelector("button").onclick = (e) => { e.preventDefault(); row.remove(); } }); await Promise.all([ this.modelListComponent.load(), this.fileListComponent.load() ]); } getSelectedData() { return { models: this.modelListComponent.getSelectedModels(), files: this.fileListComponent.getSelectedFiles(), systemPackages: Array.from(this.container.querySelectorAll("input[name='systemPackages']")) .map(input => input.value) .filter(Boolean) }; } } const buildForm = `

This feature is powered by BentoCloud, a platform for deploying
and managing ML services in customizable clusters

Name is required
Endpoint is required
API Key is required

Get your API Token at cloud.bentoml.com

` function createBuildModal() { const modal = document.createElement("div"); modal.id = "build-modal"; modal.className = "cpack-modal"; const title = document.createElement("div"); title.textContent = "Deploy Workflow to Cloud"; title.className = "cpack-title"; const form = document.createElement("form"); form.innerHTML = buildForm; const buttonContainer = document.createElement("div"); buttonContainer.className = "cpack-btn-container"; const confirmButton = document.createElement("button"); confirmButton.innerHTML = `Push to Cloud`; confirmButton.className = "cpack-btn primary"; confirmButton.style.fontWeight = "bold"; confirmButton.disabled = true; const cancelButton = document.createElement("button"); cancelButton.textContent = "Cancel"; cancelButton.className = "cpack-btn"; cancelButton.className = "cpack-btn"; buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(confirmButton); modal.appendChild(title); modal.appendChild(form); modal.appendChild(buttonContainer); const { close } = createModal(modal); cancelButton.onclick = close; return new Promise((resolve) => { form.querySelector("input[name='bentoName']").select(); const packageOptionsContainer = form.querySelector("#package-options-container"); const packageOptions = new PackageOptions(form, "build-models-list", "build-files-list", false); packageOptionsContainer.innerHTML = packageOptions.getHtml(); packageOptions.init().then(() => { confirmButton.disabled = false; }); confirmButton.onclick = async (e) => { e.preventDefault(); const formData = new FormData(form); const bentoName = formData.get("bentoName"); const endpoint = formData.get("endpoint"); const apiKey = formData.get("apiKey"); // Reset error messages form.querySelectorAll('.error-message').forEach(el => el.style.display = 'none'); // Validate required fields let hasError = false; if (!bentoName.trim()) { form.querySelector('input[name="bentoName"]').nextElementSibling.style.display = 'block'; hasError = true; } if (!endpoint.trim()) { form.querySelector('input[name="endpoint"]').nextElementSibling.style.display = 'block'; hasError = true; } if (!apiKey.trim()) { form.querySelector('input[name="apiKey"]').nextElementSibling.style.display = 'block'; hasError = true; } if (hasError) { return; } // Save values to localStorage localStorage.setItem('cpack-endpoint', endpoint); localStorage.setItem('cpack-api-key', apiKey); const { workflow, output: workflow_api } = await app.graphToPrompt(); const selectedData = packageOptions.getSelectedData(); const data = { bento_name: bentoName, system_packages: selectedData.systemPackages, push: true, api_key: apiKey, endpoint: endpoint, models: selectedData.models, files: selectedData.files, workflow, workflow_api }; close(); resolve(data); } }); } async function createServeStatusModal(url) { const modal = document.createElement("div"); modal.className = "cpack-modal"; modal.style.width = "400px"; const title = document.createElement("div"); title.textContent = "Serve Workflow as REST API"; title.className = "cpack-title"; const info = document.createElement("div"); const urlObj = new URL(url); const currentHost = window.location.hostname; const port = urlObj.port; const displayUrl = `${currentHost}:${port}`; info.innerHTML = `
🎉 Service is online! Click below to open:
${displayUrl} ↗
${spinner}
`; const buttonContainer = document.createElement("div"); buttonContainer.className = "cpack-btn-container"; const cancelButton = document.createElement("button"); cancelButton.textContent = "Stop Server"; cancelButton.className = "cpack-btn"; cancelButton.style.cssText = "background: #a63d3d; color: white; font-weight: bold"; buttonContainer.appendChild(cancelButton); modal.appendChild(title); modal.appendChild(info); modal.appendChild(buttonContainer); const { close } = createModal(modal); cancelButton.onclick = async () => { cancelButton.disabled = true; cancelButton.style.background = "#666"; try { await api.fetchApi("/bentoml/serve/terminate", { method: "POST" }); } catch(e) { console.error("Failed to stop server:", e); } clearInterval(checkInterval); close(); }; const statusInfo = info.querySelector("#status-info"); // Start status checking const checkInterval = setInterval(async () => { try { const resp = await api.fetchApi("/bentoml/serve/heartbeat"); const status = await resp.json(); if (status.error) { title.textContent = "Server Error"; statusInfo.innerHTML = `
${status.error}
`; clearInterval(checkInterval); } } catch(e) { title.textContent = "Status Check Error"; statusInfo.innerHTML = `
${e.message}
`; clearInterval(checkInterval); } }, 1000); } async function createBuildingModal(data) { const modal = document.createElement("div"); modal.className = "cpack-modal"; modal.id = "building-modal"; const title = document.createElement("div"); title.textContent = "Pushing..."; title.className = "cpack-title"; const body = document.createElement("div"); body.innerHTML = `

  

First run may take several minutes to calculate model checksums, depending on the number of models.

`; const buttonContainer = document.createElement("div"); buttonContainer.className = "cpack-btn-container"; const closeButton = document.createElement("button"); closeButton.textContent = "Close"; closeButton.className = "cpack-btn"; buttonContainer.appendChild(closeButton); modal.appendChild(title); modal.appendChild(body); modal.appendChild(buttonContainer); const { close } = createModal(modal); closeButton.onclick = close; closeButton.disabled = true; const info = body.querySelector("#build-info"); const setError = (error) => { title.textContent = "Build Failed"; info.style.display = "none"; const pre = body.querySelector("#build-error"); pre.textContent = error; pre.style.display = "block"; } info.innerHTML = spinner; try { const resp = await api.fetchApi("/bentoml/build", { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" } }); const respData = await resp.json(); if (respData.error) { setError(respData.error); } else { title.textContent = "Push Completed"; // Extract org name from endpoint const endpointUrl = new URL(data.endpoint); // Extract bento repo and version from bento tag const [repoName, version] = respData.bento.split(':'); // Construct status URL const statusUrl = `${endpointUrl.origin}/bento_repositories/${repoName}/bentos/${version}`; info.innerHTML = `
✨
Your workflow has been pushed to BentoCloud and is ready for deployment
${repoName}:${version} Deploy Now
Deployment Guide BentoCloud Console Home
`; } } catch(e) { setError(e.message); } finally { closeButton.disabled = false; } } app.registerExtension({ name: "Comfy.CPackExtension", async setup() { const styleTag = document.createElement("style"); styleTag.innerHTML = style; document.head.appendChild(styleTag); const menu = document.querySelector(".comfy-menu"); const separator = document.createElement("hr"); separator.style.margin = "20px 0"; separator.style.width = "100%"; menu.append(separator); const packButton = document.createElement("button"); packButton.textContent = "Package"; packButton.onclick = packageAction; menu.append(packButton); const unpackButton = document.createElement("button"); unpackButton.textContent = "Unpack"; unpackButton.onclick = unpackAction; menu.append(unpackButton); const serveButton = document.createElement("button"); serveButton.textContent = "Serve"; serveButton.onclick = serveAction; menu.append(serveButton); const buildButton = document.createElement("button"); buildButton.textContent = "Deploy"; buildButton.onclick = deployAction; menu.append(buildButton); try { // new style Manager buttons // unload models button into new style Manager button let cmGroup1 = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup( new(await import("../../scripts/ui/components/button.js")).ComfyButton({ icon: "package-variant-closed", action: packageAction, tooltip: "Comfy-Pack", content: "Package", classList: "comfyui-button comfyui-menu-mobile-collapse primary" }).element, new(await import("../../scripts/ui/components/button.js")).ComfyButton({ icon: "package-variant", action: unpackAction, tooltip: "Comfy-Pack", content: "Unpack", classList: "comfyui-button comfyui-menu-mobile-collapse" }).element, ); app.menu?.settingsGroup.element.before(cmGroup1.element); let cmGroup2 = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup( new(await import("../../scripts/ui/components/button.js")).ComfyButton({ icon: "api", action: serveAction, tooltip: "Comfy-Pack", content: "Serve", classList: "comfyui-button comfyui-menu-mobile-collapse primary" }).element, new(await import("../../scripts/ui/components/button.js")).ComfyButton({ icon: "cloud-upload", action: deployAction, tooltip: "Comfy-Pack", content: "Deploy", classList: "comfyui-button comfyui-menu-mobile-collapse" }).element, ); app.menu?.settingsGroup.element.before(cmGroup2.element); } catch(exception) { console.log('ComfyUI is outdated. New style menu based features are disabled.'); } } });