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
? `
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:':
'缺少以下节点:'
}
)}
${missingNodeGithub(
missingNodeTypes,
nodesMap
).join('')}
${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)
}
})
}