import { app } from '../../../scripts/app.js' import { closeIcon } from './svg_icons.js' import { api } from '../../../scripts/api.js' import { GroupNodeConfig, GroupNodeHandler } from '../../../extensions/core/groupNode.js' import { smart_init, addSmartMenu } from './smart_connect.js' import { completion_ } from './chat.js' import { getLocalData, saveLocalData, updateLLMAPIKey, convertImageUrlToBase64, get_nodes_map, injectCSS, loadCSS, loadExternalScript } from './common.js' injectCSS(` .help_link { background: linear-gradient(rgb(110 110 110 / 50%), rgba(255, 255, 0, 0)); background-size: 200% 200%; transition: background-position 0.5s; text-decoration: none; } .help_link:hover { background-position: right bottom; }`) const BIZYAIR_SERVER_ADDRESS = 'https://api.siliconflow.cn' const BIZYAIR_MODEL = '01-ai/Yi-1.5-9B-Chat-16K' function showTextByLanguage (key, json) { // 获取浏览器语言 var language = navigator.language // 判断是否为中文 if ( language.indexOf('zh') !== -1 || (language.indexOf('cn') !== -1 && json[key]) ) { return json[key] } else { return key } } //系统prompt // const systemPrompt = `You are a prompt creator, your task is to create prompts for the user input request, the prompts are image descriptions that include keywords for (an adjective, type of image, framing/composition, subject, subject appearance/action, environment, lighting situation, details of the shoot/illustration, visuals aesthetics and artists), brake keywords by comas, provide high quality, non-verboose, coherent, brief, concise, and not superfluous prompts, the subject from the input request must be included verbatim on the prompt,the prompt is english` const systemPrompt = ` Prompt: Describe a scene with a specific theme in fluent and highly detailed English, focusing on the content and style. The description should be within 100 words. Theme: [Insert Theme Here] Example: Theme: Sunset The sun sets in a blaze of orange and pink, casting a warm glow over a tranquil lake. Silhouetted trees line the shore, their reflections shimmering in the water. A lone figure sits at the end of a wooden pier, feet dangling above the mirrored surface, lost in thought. The scene exudes peacefulness and quiet beauty. ` if (!localStorage.getItem('_mixlab_system_prompt')) { localStorage.setItem('_mixlab_system_prompt', systemPrompt) } // 获取llama 模型 async function get_llamafile_models () { try { const response = await fetch('/mixlab/folder_paths', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'llamafile' }) }) const data = await response.json() // console.log(data) return data.names } catch (error) { console.error(error) } } // 运行llama async function start_llama (model = 'Phi-3-mini-4k-instruct-Q5_K_S.gguf') { let n_gpu_layers = -1 try { n_gpu_layers = parseInt(localStorage.getItem('_mixlab_llama_n_gpu')) } catch (error) {} try { const response = await fetch('/mixlab/start_llama', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model, n_gpu_layers }) }) const data = await response.json() if (data.llama_cpp_error || !data.port) { return } return { url: `http://${window.location.hostname}:${data.port}`, model: data.model, chat_format: data.chat_format } } catch (error) { console.error(error) } } function resizeImage (base64Image) { var img = new Image() var canvas = document.createElement('canvas') var ctx = canvas.getContext('2d') return new Promise((res, rej) => { img.onload = function () { // 等比例缩放图片 var width = img.width var height = img.height var max_width = 768 if (width > max_width) { height *= max_width / width width = max_width } // 设置canvas尺寸 canvas.width = width canvas.height = height // 在canvas上绘制图片 ctx.drawImage(img, 0, 0, width, height) // 将canvas转换为base64图片数据 var canvasData = canvas.toDataURL() res(canvasData) // canvas转换后的base64图片数据 } img.src = base64Image }) } const createMixlabBtn = () => { const appsButton = document.createElement('button') appsButton.id = 'mixlab_chatbot_by_llamacpp' appsButton.className = 'comfyui-button' appsButton.textContent = '♾️Mixlab' // appsButton.onclick = () => appsButton.onclick = async () => { let llm_key = await updateLLMAPIKey() // if (window._mixlab_llamacpp&&window._mixlab_llamacpp.model&&window._mixlab_llamacpp.model.length>0) { // //显示运行的模型 // createModelsModal([ // window._mixlab_llamacpp.url, // window._mixlab_llamacpp.model // ]) // } else { // // let ms = await get_llamafile_models() // // ms = ms.filter(m => !m.match('-mmproj-')) // // if (ms.length > 0) createModelsModal(ms) // } createModelsModal([], llm_key) } return appsButton } // 菜单入口 async function createMenu () { const menu = document.querySelector('.comfy-menu') const separator = document.createElement('div') separator.style = `margin: 20px 0px; width: 100%; height: 1px; background: var(--border-color); ` menu.append(separator) if ( menu.style.display === 'none' && document.querySelector('.comfyui-menu-push') ) { //新版ui document.querySelector('.comfyui-menu-push').append(createMixlabBtn()) } else { if (!menu.querySelector('#mixlab_chatbot_by_llamacpp')) { menu.append(createMixlabBtn()) } } } let isScriptLoaded = {} // function createChart (chartDom, nodes) { var myChart = echarts.init(chartDom) var option console.log(nodes) option = { series: [ { type: 'treemap', data: [ { name: 'nodeA', value: 10, children: Array.from(nodes, n => { return { name: n.type, value: n.count } }) } ] } ] } option && myChart.setOption(option) } async function createNodesCharts () { await loadExternalScript('/mixlab/app/lib/echarts.min.js') const templates = await loadTemplate() var nodes = {} Array.from(templates, t => { let j = JSON.parse(t.data) for (let node of j.nodes) { if (!nodes[node.type]) nodes[node.type] = { type: node.type, count: 0 } nodes[node.type].count++ } }) nodes = Object.values(nodes).sort((a, b) => b.count - a.count) const menu = document.querySelector('.comfy-menu') const separator = document.createElement('div') separator.style = `margin: 20px 0px; width: 100%; height: 1px; background: var(--border-color); ` menu.append(separator) const appsButton = document.createElement('button') appsButton.textContent = 'Nodes' appsButton.onclick = () => { let div = document.querySelector('#mixlab_apps') if (!div) { div = document.createElement('div') div.id = 'mixlab_apps' document.body.appendChild(div) let btn = document.createElement('div') btn.style = `display: flex; width: calc(100% - 24px); justify-content: space-between; align-items: center; padding: 0 12px; height: 44px;` let btnB = document.createElement('button') let textB = document.createElement('p') btn.appendChild(textB) btn.appendChild(btnB) textB.style.fontSize = '12px' textB.innerText = `Nodes` btnB.style = `float: right; border: none; color: var(--input-text); background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;` btnB.addEventListener('click', () => { div.style.display = 'none' }) btnB.innerText = 'X' // 悬浮框拖动事件 div.addEventListener('mousedown', function (e) { var startX = e.clientX var startY = e.clientY var offsetX = div.offsetLeft var offsetY = div.offsetTop function moveBox (e) { var newX = e.clientX var newY = e.clientY var deltaX = newX - startX var deltaY = newY - startY div.style.left = offsetX + deltaX + 'px' div.style.top = offsetY + deltaY + 'px' localStorage.setItem( 'mixlab_app_pannel', JSON.stringify({ x: div.style.left, y: div.style.top }) ) } function stopMoving () { document.removeEventListener('mousemove', moveBox) document.removeEventListener('mouseup', stopMoving) } document.addEventListener('mousemove', moveBox) document.addEventListener('mouseup', stopMoving) }) div.appendChild(btn) let chartDom = document.createElement('div') chartDom.style = `height:80vh;width:450px` chartDom.className = 'chart' div.appendChild(chartDom) } if (div.style.display == 'flex') { div.style.display = 'none' } else { let pos = JSON.parse( localStorage.getItem('mixlab_app_pannel') || JSON.stringify({ x: 0, y: 0 }) ) div.style = ` flex-direction: column; align-items: end; display:flex; position: absolute; top: ${pos.y}; left: ${pos.x}; width: 450px; color: var(--descrip-text); background-color: var(--comfy-menu-bg); padding: 10px; border: 1px solid black;z-index: 999999999;padding-top: 0;` } createChart(div.querySelector('.chart'), nodes) } menu.append(appsButton) } function copyNodeValues (src, dest) { // title dest.title = src.title // copy input connections for (let i in src.inputs) { let input = src.inputs[i] if (input.link) { let link = app.graph.links[input.link] let src_node = app.graph.getNodeById(link.origin_id) if (dest.inputs.filter(inp => inp.name === input.name).length === 0) { // 没有,name换了 let dInp = dest.inputs.filter(inp => inp.type === input.type) if (dInp.length === 1) { src_node.connect(link.origin_slot, dest.id, dInp[0].name) } } else { src_node.connect(link.origin_slot, dest.id, input.name) } } } // copy output connections let output_links = {} for (let i in src.outputs) { let output = src.outputs[i] if (output.links) { let links = [] for (let j in output.links) { links.push(app.graph.links[output.links[j]]) } output_links[output.name] = links } } for (let i in dest.outputs) { let links = output_links[dest.outputs[i].name] if (links) { for (let j in links) { let link = links[j] let target_node = app.graph.getNodeById(link.target_id) dest.connect(parseInt(i), target_node, link.target_slot) } } } // copy widgets for (const w of src.widgets) { for (const d of dest.widgets) { if (w.name === d.name) { d.value = w.value } } } app.graph.afterChange() } function deepEqual (obj1, obj2) { if (typeof obj1 !== typeof obj2) { return false } if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) { return obj1 === obj2 } const keys1 = Object.keys(obj1) const keys2 = Object.keys(obj2) if (keys1.length !== keys2.length) { return false } for (let key of keys1) { if (!deepEqual(obj1[key], obj2[key])) { return false } } return true } function get_url () { let api_host = `${window.location.hostname}:${window.location.port}` let api_base = '' let url = `${window.location.protocol}//${api_host}${api_base}` return url } async function get_my_app (filename = null, category = '') { let url = get_url() let data = null try { const res = await fetch(`${url}/mixlab/workflow`, { method: 'POST', body: JSON.stringify({ task: 'my_app', filename, category, admin: true }) }) let result = await res.json() data = [] for (const res of result.data) { let { app, workflow } = res.data if (app?.filename) data.push({ ...app, data: workflow, date: res.date }) } } catch (error) { console.log(error) } return data } var cssURL = 'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown-light.min.css' loadCSS(cssURL) injectCSS(`::-webkit-scrollbar { width: 2px; } #mixlab_chatbot_by_llamacpp{ font-size:14px } #mixlab_chatbot_by_llamacpp::before { content: attr(title); position: absolute; margin-top: 24px; font-size: 10px; } .mix_tag{ padding:8px;cursor: pointer;font-size: 14px; color: var(--input-text); background-color: var(--comfy-input-bg); border-radius: 8px; border-color: var(--border-color); border-style: solid; margin-top: 2px; margin-bottom: 14px; } .mix_tag:hover{ background-color: #101c19; color: aquamarine; } @keyframes loading_mixlab { 0% { background-color: green; } 50% { background-color: lightgreen; } 100% { background-color: green; } } .loading_mixlab { background-color: green; animation-name: loading_mixlab; animation-duration: 2s; animation-iteration-count: infinite; } .dynamic_prompt{ border-left: 2px solid var(--input-text); } `) async function getCustomnodeMappings () { let nodes = {} if (!window._nodes_maps) { const data = (await get_nodes_map()).data window._nodes_maps = data } // console.log('#getCustomnodeMappings', window._nodes_maps) for (let url in window._nodes_maps) { let n = window._nodes_maps[url] for (let node of n[0]) { // if(node=='CLIPSeg')console.log('#CLIPSeg',n) nodes[node] = { url, title: n[1].title_aux } } } return nodes } const missingNodeGithub = (missingNodeTypes, nodesMap) => { let ts = {} Array.from(new Set(missingNodeTypes), n => { if (nodesMap[n]) { let title = nodesMap[n].title if (!ts[title]) { const link = nodesMap[n].url // 判断链接是否为GitHub仓库链接 const githubRegex = /^https:\/\/github\.com\/(?:.*?\/)?([^/]+)\/.+$/ const author = link.match(githubRegex)[1] console.log(`(作者: ${author})`) ts[title] = { title, nodes: {}, url: link, author } } ts[title].nodes[n] = 1 } else { ts[n] = { title: n, nodes: {}, url: `https://github.com/search?q=${n}&type=code` } ts[n].nodes[n] = 1 } }) return Array.from(Object.values(ts), n => { const url = n.url return `${n.title}
${ n.author ? ` GitHub Logo

Author:${n.author}

` : '🔍' }
` }) } let nodesMap // Enhanced navigation to GitHub for missing node search upon graph load. // 更好地错误提示,找到GitHub原仓库地址 app.showMissingNodesError = async function ( missingNodeTypes, hasAddedNodes = true ) { nodesMap = nodesMap && Object.keys(nodesMap).length > 0 ? nodesMap : await getCustomnodeMappings() this.ui.dialog.show( `${showTextByLanguage( 'When loading the graph, the following node types were not found:', { 'When loading the graph, the following node types were not found:': '缺少以下节点:' } )} ${hasAddedNodes ? '' : ''}

${showTextByLanguage( 'Welcome to Mixlab nodes discord, seeking help.', { 'Welcome to Mixlab nodes discord, seeking help.': '寻求帮助,加入Mixlab nodes交流频道' } )} ` ) this.logging.addEntry('Comfy.App', 'warn', { MissingNodes: missingNodeTypes }) } // 读取仓库说明 async function fetchReadmeContent (url) { try { // var repo = 'owner/repo'; // 仓库的拥有者和名称 var match = url.match(/github.com\/([^/]+\/[^/]+)/) var repo = match[1] var url = `https://api.github.com/repos/${repo}/readme` var response = await fetch(url) var data = await response.json() var readmeUrl = data.download_url var readmeResponse = await fetch(readmeUrl) var content = await readmeResponse.text() // console.log(content) // 在控制台输出readme.md文件的内容 return content } catch (error) { console.log('获取readme.md文件信息失败:', error) } } function createInputOfLabel (labelText, key, id) { const label = document.createElement('p') label.innerText = labelText const input = document.createElement('input') input.type = 'text' input.style = `color: var(--input-text); background-color: var(--comfy-input-bg); border-radius: 8px; border-color: var(--border-color); height: 26px; padding: 4px 10px; width: 150px; margin-left: 12px;` input.value = getLocalData(key)['-'] || Object.values(getLocalData(key))[0] || 'by Mixlab' input.addEventListener('change', e => { e.stopPropagation() e.preventDefault() saveLocalData(key, '-', input.value) }) const div = document.createElement('div') div.style = `display: flex; justify-content: flex-start; align-items: baseline;padding: 0 18px;` div.addEventListener('click', e => { e.stopPropagation() }) div.appendChild(label) div.appendChild(input) return div } function createModelsModal (models, llmKey) { var div = document.querySelector('#model-modal') || document.createElement('div') div.id = 'model-modal' div.innerHTML = '' div.style.cssText = ` width: 100%; z-index: 9990; height: 100vh; display: flex; color: var(--descrip-text); position: fixed; top: 0; left: 0; background: #000000a8; ` var modal = document.createElement('div') div.addEventListener('click', e => { e.stopPropagation() div.remove() }) div.appendChild(modal) modal.classList.add('modal-body') // Set modal styles modal.style.cssText = ` color: var(--descrip-text); background-color: var(--comfy-menu-bg); position: fixed; overflow:hidden; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999; border-radius: 4px; box-shadow: 4px 4px 14px rgba(255,255,255,0.2); ` // Create modal header const headerElement = document.createElement('div') headerElement.classList.add('modal-header') headerElement.style.cssText = ` display: flex; padding: 20px 24px 8px 24px; justify-content: space-between; ` const headTitleElement = document.createElement('a') headTitleElement.classList.add('header-title') headTitleElement.style.cssText = ` color: var(--descrip-text); font-size: 18px; display: flex; align-items: flex-start; flex: 1; overflow: hidden; text-decoration: none; font-weight: bold; justify-content: space-between; padding: 20px; cursor: pointer; user-select: none; ` const linkIcon = document.createElement('small') linkIcon.textContent = showTextByLanguage('Auto Open', { 'Auto Open': '自动开启' }) linkIcon.style.padding = '4px' const statusIcon = document.createElement('small') statusIcon.textContent = showTextByLanguage('Status', { Status: 'OFF' }) statusIcon.id = 'llm_status_btn' statusIcon.style = `padding: 4px; background-color: rgb(102, 255, 108); color: black; font-size: 12px; margin-left: 12px;` if (window._mixlab_llamacpp?.url) { statusIcon.textContent = window._mixlab_llamacpp.model statusIcon.style.backgroundColor = '#66ff6c' statusIcon.style.color = 'black' } else { } statusIcon.addEventListener('click', e => { e.stopPropagation() // startLLM() }) const batchPageBtn = document.createElement('div') batchPageBtn.style = `display: flex; justify-content: center; align-items: center; font-size: 12px;` batchPageBtn.innerHTML = `MixLab App` const siliconflowHelp = document.createElement('a') siliconflowHelp.textContent = showTextByLanguage('Use Siliconflow', { 'Use Siliconflow': '使用硅基流动' }) + '\n' + showTextByLanguage('Or Local LLM', { 'Or Local LLM': '或者本地LLM' }) siliconflowHelp.style = `color: var(--input-text); background-color: var(--comfy-input-bg);margin-top:14px;font-size: 16px;` siliconflowHelp.href = 'https://cloud.siliconflow.cn/s/mixlabs' siliconflowHelp.target = '_blank' const title = document.createElement('p') title.innerText = 'Mixlab Nodes' title.style = `font-size: 18px; margin-right: 8px; margin-top: 0;` const left_d = document.createElement('div') left_d.style = `display: flex; justify-content: center; align-items: flex-start; font-size: 12px; flex-direction: column; ` left_d.appendChild(title) left_d.appendChild(batchPageBtn) left_d.appendChild(siliconflowHelp) headTitleElement.appendChild(left_d) //重启 const reStart = document.createElement('small') reStart.textContent = showTextByLanguage('restart', { restart: '重启' }) reStart.style = `padding: 8px; font-size: 16px; outline: 1px solid; padding-top: 4px; padding-bottom: 4px;` headTitleElement.appendChild(reStart) if (localStorage.getItem('_mixlab_auto_llama_open')) { linkIcon.style.backgroundColor = '#66ff6c' linkIcon.style.color = 'black' } linkIcon.addEventListener('click', e => { e.stopPropagation() if (localStorage.getItem('_mixlab_auto_llama_open')) { localStorage.setItem('_mixlab_auto_llama_open', '') linkIcon.style.backgroundColor = '' linkIcon.style.color = 'var(--descrip-text)' } else { localStorage.setItem('_mixlab_auto_llama_open', 'true') linkIcon.style.backgroundColor = '#66ff6c' linkIcon.style.color = 'black' } }) reStart.addEventListener('click', e => { e.stopPropagation() div.remove() fetch('mixlab/re_start', { method: 'POST' }) }) modal.appendChild(headTitleElement) // Create modal content area var modalContent = document.createElement('div') modalContent.classList.add('modal-content') let llmKeyDiv = createInputOfLabel('LLM Key', '_mixlab_llm_api_key', '-') if (!getLocalData('_mixlab_llm_api_url')['-']) { saveLocalData('_mixlab_llm_api_url', '-', BIZYAIR_SERVER_ADDRESS) } let llmAPIDiv = createInputOfLabel('LLM API', '_mixlab_llm_api_url', '-') if (!getLocalData('_mixlab_llm_model_name')['-']) { saveLocalData('_mixlab_llm_model_name', '-', BIZYAIR_MODEL) } let llmModelDiv = createInputOfLabel( 'LLM Model', '_mixlab_llm_model_name', '-' ) modalContent.appendChild(llmKeyDiv) modalContent.appendChild(llmAPIDiv) modalContent.appendChild(llmModelDiv) var inputForSystemPrompt = document.createElement('textarea') inputForSystemPrompt.className = 'comfy-multiline-input' inputForSystemPrompt.style = `height: 260px;width: 480px;font-size: 16px;padding: 18px;` inputForSystemPrompt.value = localStorage.getItem('_mixlab_system_prompt') inputForSystemPrompt.addEventListener('change', e => { e.stopPropagation() localStorage.setItem('_mixlab_system_prompt', inputForSystemPrompt.value) }) inputForSystemPrompt.addEventListener('click', e => { e.stopPropagation() }) modalContent.appendChild(inputForSystemPrompt) modal.appendChild(modalContent) const helpInfo = document.createElement('a') helpInfo.textContent = showTextByLanguage('Help', { Help: '寻求帮助' }) helpInfo.style = `text-align: center; display: block; padding: 8px; cursor: pointer; font-size: 12px; color: white;` helpInfo.href = 'https://discord.gg/cXs9vZSqeK' helpInfo.target = '_blank' modal.appendChild(helpInfo) document.body.appendChild(div) } function createModal (url, markdown, title) { // Create modal element var div = document.querySelector('#mix-modal') || document.createElement('div') div.id = 'mix-modal' div.innerHTML = '' div.style.cssText = ` width: 100%; z-index: 9990; height: 100vh; display: flex; color: var(--descrip-text); position: fixed; top: 0; left: 0; ` var modal = document.createElement('div') div.appendChild(modal) modal.classList.add('modal-body') // Set modal styles modal.style.cssText = ` background: white; height: 80vh; position: fixed; overflow:hidden; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999; border-radius: 4px; box-shadow: 4px 4px 14px rgba(255,255,255,0.5); ` // Create modal content area var modalContent = document.createElement('div') modalContent.classList.add('modal-content') // Create modal header const headerElement = document.createElement('div') headerElement.classList.add('modal-header') headerElement.style.cssText = ` display: flex; padding: 20px 24px 8px 24px; justify-content: space-between; ` const headTitleElement = document.createElement('a') headTitleElement.classList.add('header-title') headTitleElement.style.cssText = ` color: var(--descrip-text); font-size: 18px; display: flex; align-items: center; flex: 1; overflow: hidden; text-decoration: none; font-weight: bold; ` headTitleElement.onmouseenter = function () { headTitleElement.style.color = 'var(--comfy-menu-bg)' } headTitleElement.onmouseleave = function () { headTitleElement.style.color = 'var(--descrip-text)' } headTitleElement.textContent = title || '' headTitleElement.href = url headTitleElement.target = '_blank' const linkIcon = document.createElement('small') linkIcon.textContent = '🔗' headTitleElement.appendChild(linkIcon) headerElement.appendChild(headTitleElement) // Create close button const closeButton = document.createElement('span') closeButton.classList.add('close') closeButton.innerHTML = closeIcon // Set close button styles closeButton.style.cssText = ` padding: 4px; cursor: pointer; width: 32px; height: 32px; display: flex; justify-content: center; align-items: center; user-select: none; fill: var(--descrip-text); ` closeButton.onmouseenter = function () { closeButton.style.fill = 'var(--comfy-menu-bg)' } closeButton.onmouseleave = function () { closeButton.style.fill = 'var(--descrip-text)' } headerElement.appendChild(closeButton) // Click event to close the modal function closeMixModal () { div.style.display = 'none' window.removeEventListener('keydown', MixModalEscKeyEvent) } closeButton.onclick = function () { closeMixModal() } // Set modal content area styles modalContent.style.cssText = ` position: relative; padding: 0px; overflow: hidden scroll;; height: 100%; min-width:300px ` // Append close button to modal content area modal.appendChild(headerElement) // Create element for displaying Markdown content var markdownContent = document.createElement('div') markdownContent.classList.add('markdown-content', 'markdown-body') markdownContent.style.cssText = `max-width: 50vw;padding: 0px 24px 100px 24px;` showdown.setFlavor('github') var converter = new showdown.Converter() var html = converter.makeHtml(markdown) // Hide images in the markdown when they fail to load var regex = /]+src="?([^"\s]+)"?[^>]*>/g html = html.replace(regex, function (match, src) { return `` }) // Open links in a new tab or window html = html.replace(/]+href=["'])(?!https?:\/\/)([^"'>]+)/g, function (match, prefix, path) { var absolutePath = url + '/' + path return ' { const id = 'Comfy.NodeTemplates' const file = 'comfy.templates.json' let templates = [] if (app.storageLocation === 'server') { if (app.isNewUserSession) { // New user so migrate existing templates const json = localStorage.getItem(id) if (json) { templates = JSON.parse(json) } await api.storeUserData(file, json, { stringify: false }) } else { const res = await api.getUserData(file) if (res.status === 200) { try { templates = await res.json() } catch (error) {} } else if (res.status !== 404) { console.error(res.status + ' ' + res.statusText) } } } else { const json = localStorage.getItem(id) if (json) { templates = JSON.parse(json) } } return templates ?? [] } function drawBadge (node, orig, restArgs) { let ctx = restArgs[0] const r = orig?.apply?.(node, restArgs) if ( !node.flags.collapsed && node.constructor.title_mode != LiteGraph.NO_TITLE ) { let text = `#${node.id} ` let nick = node.getNickname() if (nick) { if (nick == 'ComfyUI') { nick = '🦊' } if (nick.length > 25) { text += nick.substring(0, 23) + '..' } else { text += nick } } if (text != '') { let fgColor = 'white' let bgColor = '#0F1F0F' let visible = true ctx.save() ctx.font = '12px sans-serif' const sz = ctx.measureText(text) ctx.fillStyle = bgColor ctx.beginPath() ctx.roundRect( node.size[0] - sz.width - 12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5 ) ctx.fill() ctx.fillStyle = fgColor ctx.fillText( text, node.size[0] - sz.width - 6, -LiteGraph.NODE_TITLE_HEIGHT - 6 ) ctx.restore() if (node.has_errors) { ctx.save() ctx.font = 'bold 14px sans-serif' const sz2 = ctx.measureText(node.type) ctx.fillStyle = 'white' ctx.fillText( node.type, node.size[0] / 2 - sz2.width / 2, node.size[1] / 2 ) ctx.restore() } } } return r } async function getSelectImageNode () { var nodes = app.canvas.selected_nodes let imageNode = null if (Object.keys(app.canvas.selected_nodes).length == 0) return for (var id in nodes) { if (nodes[id].imgs) { let base64 = await convertImageUrlToBase64(nodes[id].imgs[0].currentSrc) imageNode = await resizeImage(base64) } } return imageNode } app.registerExtension({ name: 'Comfy.Mixlab.ui', init () { //是否要自动加载模型 // if (localStorage.getItem('_mixlab_auto_llama_open')) { // let model = localStorage.getItem('_mixlab_llama_select') // start_llama(model).then(res => { // window._mixlab_llamacpp = res // document.body // .querySelector('#mixlab_chatbot_by_llamacpp') // .setAttribute('title', res.url) // }) // } else { // // startLLM('') // } LGraphCanvas.prototype.helpAboutNode = async function (node) { nodesMap = nodesMap && Object.keys(nodesMap).length > 0 ? nodesMap : await getCustomnodeMappings() console.log( '%c### node & node map', 'background: yellow; color: black', node, nodesMap, nodesMap[node.type] ) let repo = nodesMap[node.type] if (repo) { let markdown = await fetchReadmeContent(repo.url) await loadExternalScript('/mixlab/app/lib/showdown.min.js') createModal(repo.url, markdown, repo.title) } } LGraphCanvas.prototype.fixTheNode = function (node) { let new_node = LiteGraph.createNode(node.comfyClass) console.log(node) if (new_node) { new_node.pos = [node.pos[0], node.pos[1]] app.canvas.graph.add(new_node, false) copyNodeValues(node, new_node) app.canvas.graph.remove(node) } } smart_init() LGraphCanvas.prototype.text2text = async function (node) { let widget = node.widgets.filter( w => (w.name === 'text' || w.name === 'prompt') && typeof w.value == 'string' )[0] if (widget) { app.canvas.centerOnNode(node) let controller = new AbortController() let ends = [] //TODO 判断终止 <|im_start|> let userInput = widget.value widget.value = widget.value.trim() widget.value += '\n' let jsonStr = '' try { await completion_( getLocalData('_mixlab_llm_api_key')['-'] || Object.values(getLocalData('_mixlab_llm_api_key'))[0], getLocalData('_mixlab_llm_api_url')['-'] || Object.values(getLocalData('_mixlab_llm_api_url'))[0], getLocalData('_mixlab_llm_model_name')['-'] || Object.values(getLocalData('_mixlab_llm_model_name'))[0], [ { role: 'system', content: localStorage.getItem('_mixlab_system_prompt') }, { role: 'user', content: userInput } ], controller, t => { let content = t.data?.choices[0]?.delta?.content || '' console.log(content) widget.value += content // jsonStr += content } ) } catch (error) { console.log(error) } } } LGraphCanvas.prototype.image2text = async function (node) { let imageBase64 = await getSelectImageNode() if (imageBase64) { // console.log('image2text') // 添加note 节点 const NoteNode = LiteGraph.createNode('Note') NoteNode.title = `Image-to-Text ${node.id}` NoteNode.size = [NoteNode.size[0] + 100, NoteNode.size[1]] let widget = NoteNode.widgets[0] widget.value = '' NoteNode.pos = [node.pos[0] + node.size[0] + 24, node.pos[1] - 48] app.canvas.graph.add(NoteNode, false) app.canvas.centerOnNode(NoteNode) let controller = new AbortController() let ends = [] let userInput = widget.value widget.value = widget.value.trim() widget.value += '\n' try { // await completion_( // window._mixlab_llamacpp.url + '/v1/chat/completions', // [ // { // role: 'system', // content: localStorage.getItem('_mixlab_system_prompt') // }, // // { role: 'user', content: userInput } // { // role: 'user', // content: [ // { // type: 'image_url', // image_url: { // url: imageBase64 // } // }, // { type: 'text', text: 'What’s in this image?' } // ] // } // ], // controller, // t => { // // console.log(t) // widget.value += t // NoteNode.size[1] = widget.element.scrollHeight + 20 // widget.computedHeight = NoteNode.size[1] // app.canvas.centerOnNode(NoteNode) // } // ) } catch (error) { //是否要自动加载模型 // if (localStorage.getItem('_mixlab_auto_llama_open')) { // let model = localStorage.getItem('_mixlab_llama_select') // start_llama(model).then(async res => { // window._mixlab_llamacpp = res // document.body // .querySelector('#mixlab_chatbot_by_llamacpp') // .setAttribute('title', res.url) // await completion_( // window._mixlab_llamacpp.url + '/v1/chat/completions', // [ // { // role: 'system', // content: localStorage.getItem('_mixlab_system_prompt') // }, // { // role: 'user', // content: [ // { // type: 'image_url', // image_url: { // url: imageBase64 // } // }, // { type: 'text', text: 'What’s in this image?' } // ] // } // ], // controller, // t => { // // console.log(t) // widget.value += t // NoteNode.size[1] = widget.element.scrollHeight + 20 // widget.computedHeight = NoteNode.size[1] // app.canvas.centerOnNode(NoteNode) // } // ) // }) // } } widget.value = widget.value.trim() } } const getGroupMenuOptions = LGraphCanvas.prototype.getGroupMenuOptions // store the existing method LGraphCanvas.prototype.getGroupMenuOptions = function (node) { // replace it const options = getGroupMenuOptions.apply(this, arguments) // start by calling the stored one node.setDirtyCanvas(true, true) // force a redraw of (foreground, background) return [ { content: 'Clone Group ♾️Mixlab', // with a name callback: async (value, opts, e, menu, group) => { const clipboardAction = async cb => { // We use the clipboard functions but dont want to overwrite the current user clipboard // Restore it after we've run our callback const old = localStorage.getItem('litegrapheditor_clipboard') await cb() localStorage.setItem('litegrapheditor_clipboard', old) } clipboardAction(async () => { let name = group.title let nodes = group._nodes app.canvas.copyToClipboard(nodes) let data = localStorage.getItem('litegrapheditor_clipboard') data = JSON.parse(data) for (let i = 0; i < nodes.length; i++) { const node = app.graph.getNodeById(nodes[i].id) const nodeData = node.serialize() let groupData = GroupNodeHandler.getGroupData(node) if (groupData) { groupData = groupData.nodeData if (!data.groupNodes) { data.groupNodes = {} } data.groupNodes[nodeData.name] = groupData data.nodes[i].type = nodeData.name } } await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {}) localStorage.setItem( 'litegrapheditor_clipboard', JSON.stringify(data) ) app.canvas.pasteFromClipboard() }) } // and the callback }, { content: 'Save Group as Template ♾️Mixlab', // with a name callback: async (value, opts, e, menu, group) => { // console.log(options) const clipboardAction = async cb => { // We use the clipboard functions but dont want to overwrite the current user clipboard // Restore it after we've run our callback const old = localStorage.getItem('litegrapheditor_clipboard') await cb() localStorage.setItem('litegrapheditor_clipboard', old) } clipboardAction(async () => { let name = group.title + ' ♾️Mixlab' let nodes = group._nodes app.canvas.copyToClipboard(nodes) let data = localStorage.getItem('litegrapheditor_clipboard') data = JSON.parse(data) for (let i = 0; i < nodes.length; i++) { const node = app.graph.getNodeById(nodes[i].id) const nodeData = node.serialize() let groupData = GroupNodeHandler.getGroupData(node) // console.log('groupData',GroupNodeHandler.isGroupNode(node),groupData) if (groupData) { groupData = groupData.nodeData if (!data.groupNodes) { data.groupNodes = {} } data.groupNodes[nodeData.name] = groupData data.nodes[i].type = nodeData.name } } // templete const store = async nt => { const id = 'Comfy.NodeTemplates' const file = 'comfy.templates.json' let templates = await loadTemplate() templates.push(nt) if (app.storageLocation === 'server') { const ts = JSON.stringify(templates, undefined, 4) localStorage.setItem(id, ts) // Backwards compatibility try { await api.storeUserData(file, ts, { stringify: false }) } catch (error) { console.error(error) alert(error.message) } } else { localStorage.setItem(id, JSON.stringify(templates)) } } console.log('data', data) store({ name, data: JSON.stringify(data) }) }) } // and the callback }, { content: `Remove Group&Nodes ♾️Mixlab`, // with a name callback: async (value, opts, e, menu, group) => { // console.log(group) let nodes = group._nodes for (const node of nodes) { app.graph.remove(node) } app.graph.remove(group) } // and the callback }, null, ...options ] // and return the options } LGraphCanvas.prototype.centerOnNode = function (node) { // console.log(node) var dpr = window.devicePixelRatio || 1 // 获取设备像素比 this.ds.offset[0] = -node.pos[0] - node.size[0] * 0.5 + (this.canvas.width * 0.5) / (this.ds.scale * dpr) // 考虑设备像素比 this.ds.offset[1] = -node.pos[1] - node.size[1] * 0.5 + (this.canvas.height * 0.5) / (this.ds.scale * dpr) // 考虑设备像素比 this.setDirty(true, true) } const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions LGraphCanvas.prototype.getNodeMenuOptions = function (node) { // replace it const options = getNodeMenuOptions.apply(this, arguments) // start by calling the stored one node.setDirtyCanvas(true, true) // force a redraw of (foreground, background) let opts = [ { content: 'Help ♾️Mixlab', // with a name callback: () => { // console.log('#data',node) LGraphCanvas.prototype.helpAboutNode(node) } // and the callback }, { content: 'Fix node v2', // with a name callback: () => { LGraphCanvas.prototype.fixTheNode(node) } } ] if (node.widgets) { let text_widget = node.widgets.filter( w => (w.name === 'text' || w.name === 'prompt') && typeof w.value == 'string' ) let text_input = node.inputs?.filter( inp => inp.name == 'text' && inp.type == 'STRING' ) const llm_api_key = getLocalData('_mixlab_llm_api_key')['-'] || Object.values(getLocalData('_mixlab_llm_api_key'))[0], llm_api_url = getLocalData('_mixlab_llm_api_url')['-'] || Object.values(getLocalData('_mixlab_llm_api_url'))[0] if ( text_widget && text_widget.length == 1 && llm_api_key && llm_api_url && node.type != 'ShowTextForGPT' ) { opts.push({ content: 'Text-to-Text ♾️Mixlab', // with a name callback: () => { LGraphCanvas.prototype.text2text(node) } // and the callback }) } // if ( // node.imgs && // node.imgs.length > 0 && // window._mixlab_llamacpp && // window._mixlab_llamacpp.chat_format === 'llava-1-5' // ) { // opts.push({ // content: 'Image-to-Text ♾️Mixlab', // with a name // callback: () => { // LGraphCanvas.prototype.image2text(node) // } // and the callback // }) // } } return [...opts, null, ...options] // and return the options } // 支持app模式的json const loadAppJson = async data => { let workflow try { let w = JSON.parse(data) if (w.app && w.output) workflow = w.workflow } catch (err) {} if (workflow && workflow.version && workflow.nodes && workflow.extra) { await app.loadGraphData(workflow) } } if (!window._mixlab_app_paste_listener) { window._mixlab_app_paste_listener = true //粘贴json的事件 document.addEventListener('paste', async e => { // ctrl+shift+v is used to paste nodes with connections // this is handled by litegraph if (this.shiftDown) return let data = e.clipboardData || window.clipboardData // No image found. Look for node data data = data.getData('text/plain') loadAppJson(data) }) // 把json往里 拖 document.addEventListener('drop', async event => { event.preventDefault() event.stopPropagation() // Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that if ( event.dataTransfer.files.length && event.dataTransfer.files[0].type == 'application/json' ) { const reader = new FileReader() reader.onload = async () => { loadAppJson(reader.result) } reader.readAsText(event.dataTransfer.files[0]) } }) } createMenu() }, setup () { setTimeout(async () => { // Add canvas menu options const orig = LGraphCanvas.prototype.getCanvasMenuOptions // const apps = await get_my_app() // if (!apps) return // console.log('apps', apps) // let apps_map = { 0: [] } // for (const app of apps) { // if (app.category) { // if (!apps_map[app.category]) apps_map[app.category] = [] // apps_map[app.category].push(app) // } else { // apps_map['0'].push(app) // } // } let apps_opts = [] // for (const category in apps_map) { // // console.log('category', typeof category) // if (category === '0') { // apps_opts.push( // ...Array.from(apps_map[category], a => { // // console.log('#1级',a) // return { // content: `${a.name}_${a.version}`, // has_submenu: false, // callback: async () => { // try { // let ddd = await get_my_app(a.filename) // if (!ddd) return // let item = ddd[0] // if (item) { // if (item.author) { // // 有作者信息 // if (item.author.avatar) // localStorage.setItem( // '_mixlab_author_avatar', // item.author.avatar // ) // if (item.author.name) // localStorage.setItem( // '_mixlab_author_name', // item.author.name // ) // if (item.author.link) // localStorage.setItem( // '_mixlab_author_link', // item.author.link // ) // } // // console.log(item.data) // app.loadGraphData(item.data) // setTimeout(() => { // const node = app.graph._nodes_in_order[0] // if (!node) return // app.canvas.centerOnNode(node) // app.canvas.setZoom(0.5) // }, 1000) // } // } catch (error) {} // } // } // }) // ) // } else { // // 二级 // apps_opts.push({ // content: '🚀 ' + category, // has_submenu: true, // disabled: false, // submenu: { // options: Array.from(apps_map[category], a => { // // console.log('#二级',a) // return { // content: `${a.name}_${a.version}`, // callback: async () => { // try { // let ddd = await get_my_app(a.filename, a.category) // if (!ddd) return // let item = ddd[0] // if (item) { // console.log(item) // if (item.author) { // // 有作者信息 // if (item.author.avatar) // localStorage.setItem( // '_mixlab_author_avatar', // item.author.avatar // ) // if (item.author.name) // localStorage.setItem( // '_mixlab_author_name', // item.author.name // ) // if (item.author.link) // localStorage.setItem( // '_mixlab_author_link', // item.author.link // ) // } // // console.log(item.data) // app.loadGraphData(item.data) // setTimeout(() => { // const node = app.graph._nodes_in_order[0] // if (!node) return // app.canvas.centerOnNode(node) // app.canvas.setZoom(0.5) // }, 1000) // } // } catch (error) {} // } // } // }) // } // }) // } // } // console.log('apps',apps_map, apps_opts,apps) LGraphCanvas.prototype.getCanvasMenuOptions = function () { const options = orig.apply(this, arguments) options.push( null, { content: `Nodes Map ♾️Mixlab`, disabled: false, callback: async () => { nodesMap = nodesMap && Object.keys(nodesMap).length > 0 ? nodesMap : await getCustomnodeMappings() const nodesDiv = document.createDocumentFragment() const nodes = (await app.graphToPrompt()).output // console.log('[Mixlab]', 'loaded graph node: ', app) let div = document.querySelector('#mixlab_find_the_node') || document.createElement('div') div.id = 'mixlab_find_the_node' div.style = ` flex-direction: column; align-items: end; display:flex;position: absolute; top: 50px; left: 50px; width: 200px; color: var(--descrip-text); background-color: var(--comfy-menu-bg); padding: 10px; border: 1px solid black;z-index: 999999999;padding-top: 0;` div.innerHTML = '' let btn = document.createElement('div') btn.style = `display: flex; width: calc(100% - 24px); justify-content: space-between; align-items: center; padding: 0 12px; height: 44px;` let btnB = document.createElement('button') let textB = document.createElement('p') btn.appendChild(textB) btn.appendChild(btnB) textB.style.fontSize = '12px' textB.innerText = `Locate and navigate nodes ♾️Mixlab` btnB.style = `float: right; border: none; color: var(--input-text); background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;` btnB.addEventListener('click', () => { div.style.display = 'none' }) btnB.innerText = 'X' // 悬浮框拖动事件 div.addEventListener('mousedown', function (e) { var startX = e.clientX var startY = e.clientY var offsetX = div.offsetLeft var offsetY = div.offsetTop function moveBox (e) { var newX = e.clientX var newY = e.clientY var deltaX = newX - startX var deltaY = newY - startY div.style.left = offsetX + deltaX + 'px' div.style.top = offsetY + deltaY + 'px' } function stopMoving () { document.removeEventListener('mousemove', moveBox) document.removeEventListener('mouseup', stopMoving) } document.addEventListener('mousemove', moveBox) document.addEventListener('mouseup', stopMoving) }) div.appendChild(btn) const updateNodes = (ns, nd) => { let appInfoNodes = {} try { let appInfo = app.graph._nodes.filter( n => n.type === 'AppInfo' )[0] if (appInfo) { appInfoNodes[appInfo.id] = 2 for (const id of appInfo.widgets[1].value.split('\n')) { if (id && id.trim() && parseInt(id)) { appInfoNodes[id] = 0 } } for (const id of app.graph._nodes .filter(n => n.type === 'AppInfo')[0] .widgets[2].value.split('\n')) { if (id && id.trim() && parseInt(id)) { appInfoNodes[id] = 1 } } } } catch (error) { console.log(error) } for (let nodeId in ns) { let n = ns[nodeId].title || ns[nodeId].class_type if (nodesMap[n]) { const { url, title } = nodesMap[n] let d = document.createElement('button') d.style = `text-align: left; margin:6px; color: var(--input-text); background-color: var(--comfy-input-bg); border-color: ${ appInfoNodes[nodeId] >= 0 ? appInfoNodes[nodeId] === 1 ? 'blue' : 'red' : 'var(--border-color)' }; cursor: pointer;` if (appInfoNodes[nodeId] === 2) { // appinfo d.style.backgroundColor = '#326328' d.style.color = '#ffffff' d.style.borderColor = 'transparent' } d.addEventListener('click', () => { // console.log('node') const node = app.graph.getNodeById(nodeId) if (!node) return app.canvas.centerOnNode(node) app.canvas.setZoom(1) }) d.addEventListener('mouseover', async () => { // console.log('mouseover') let n = (await app.graphToPrompt()).output if (!deepEqual(n, ns)) { nd.innerHTML = '' updateNodes(n, nd) } }) d.innerHTML = ` ${'#' + nodeId} ${n} 🔗 ` d.title = title nd.appendChild(d) } } } let nodesDivv = document.createElement('div') let appInfoNodes = {} try { let appInfo = app.graph._nodes.filter( n => n.type === 'AppInfo' )[0] if (appInfo) { appInfoNodes[appInfo.id] = 2 for (const id of appInfo.widgets[1].value.split('\n')) { if (id && id.trim() && parseInt(id)) { appInfoNodes[id] = 0 } } for (const id of app.graph._nodes .filter(n => n.type === 'AppInfo')[0] .widgets[2].value.split('\n')) { if (id && id.trim() && parseInt(id)) { appInfoNodes[id] = 1 } } } } catch (error) { console.log(error) } for (let nodeId in nodes) { let n = nodes[nodeId].class_type if (nodesMap[n]) { const { url, title: _title } = nodesMap[n] let title = app.graph.getNodeById(nodeId).title || _title let d = document.createElement('button') d.style = `text-align: left; margin:6px; color: var(--input-text); background-color: var(--comfy-input-bg); border-color: ${ appInfoNodes[nodeId] >= 0 ? appInfoNodes[nodeId] === 1 ? 'blue' : 'red' : 'var(--border-color)' }; cursor: pointer;` if (appInfoNodes[nodeId] === 2) { // appinfo d.style.backgroundColor = '#326328' d.style.color = '#ffffff' d.style.borderColor = 'transparent' } d.addEventListener('click', () => { console.log('click') const node = app.graph.getNodeById(nodeId) if (!node) return app.canvas.centerOnNode(node) app.canvas.setZoom(1) }) d.addEventListener('mouseover', async () => { // console.log('mouseover') let n = (await app.graphToPrompt()).output if (!deepEqual(n, nodes)) { nodesDivv.innerHTML = '' updateNodes(n, nodesDivv) } }) d.innerHTML = ` ${'#' + nodeId} ${title} 🔗 ` d.title = n nodesDiv.appendChild(d) } } nodesDivv.appendChild(nodesDiv) nodesDivv.style = `overflow: scroll; height: 70vh;width: 100%;` div.appendChild(nodesDivv) if (!document.querySelector('#mixlab_find_the_node')) document.body.appendChild(div) } }, apps_opts.length > 0 ? { content: 'Workflow App ♾️Mixlab', has_submenu: true, disabled: false, submenu: { options: apps_opts } } : null ) return options } }, 1000) // createNodesCharts() }, nodeCreated (node) { if (node.widgets) { // Locate dynamic prompt text widgets // Include any widgets with dynamicPrompts set to true, and customtext for (let index = 0; index < node.widgets.length; index++) { const widget = node.widgets[index] if ( (widget.type === 'customtext' && widget.dynamicPrompts !== false) || widget.dynamicPrompts ) { widget.element.classList.add('dynamic_prompt') widget.element.addEventListener('mouseover', e => { // console.log(node.widgets_values[index]) if (node.widgets_values && node.widgets_values[index]) widget.element.setAttribute('title', node.widgets_values[index]) }) } } } fetch('manager/badge_mode').then(r => { if (r.status === 404) { // 右上角的badge是否已经绘制 if (!node.badge_enabled) { if (!node.getNickname) { node.getNickname = function () { if (node.nickname) { return node.nickname } return // return getNickname(node, node.comfyClass.trim()) } } const orig = node.__proto__.onDrawForeground node.onDrawForeground = function (ctx) { drawBadge(node, orig, arguments) } node.badge_enabled = true } } }) }, async loadedGraphNode (node, app) { // console.log( // '#ui init', // app.graph._nodes[app.graph._nodes.length - 1].id, // node.id // ) try { // 用来居中显示节点 if ((app.graph._nodes[app.graph._nodes.length - 1].id, node.id)) { app.canvas.centerOnNode(node) app.canvas.setZoom(0.45) } } catch (error) {} } }) //获取当前显存 function fetchSystemStats () { return new Promise(async (resolve, reject) => { try { const response = await fetch('/system_stats') const data = await response.json() resolve(data) } catch (error) { reject(error) } }) } //清理显存 function postFreeData () { return new Promise(async (resolve, reject) => { try { const postData = { unload_models: true, free_memory: true } const response = await fetch('/free', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(postData) }) if (response.ok) { resolve() } else { reject(new Error('Request failed')) } } catch (error) { reject(error) } }) }