Merge pull request #43 from AIDC-AI/parameter_debug_dev

Parameter debug
This commit is contained in:
ai-business-hql
2025-04-07 22:53:31 +08:00
committed by GitHub
33 changed files with 6254 additions and 231 deletions
-6
View File
@@ -1,6 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-CnVi6gIQ.js","copilot_web/vendor-markdown-Dek94WS0.js","copilot_web/vendor-react-V04_Axys.js","copilot_web/message-components-B6pkQvDp.js","copilot_web/input.js","copilot_web/assets/input-CJq8z6H1.css"].map(path => {
const apiBase = window.comfyAPI?.api?.api?.api_base;
const prefix = apiBase ? `${apiBase.substring(1)}/` : '';
return `${prefix}${path}`;
}))))=>i.map(i=>d[i]);
import{_ as E}from"./input.js";import{j as s}from"./vendor-markdown-Dek94WS0.js";import{r as t,R as w}from"./vendor-react-V04_Axys.js";import{C as v}from"./message-components-B6pkQvDp.js";const y=9999999,x={EXPLAIN_NODE:"copilot:explain-node"},D="/copilot_web/assets/logo-BTZhX0BN.png";function _(n={}){const[u,l]=t.useState({x:n.initialX??window.innerWidth-160,y:n.initialY??20}),[o,r]=t.useState(!1),[a,m]=t.useState({x:0,y:0}),[d,c]=t.useState(0),[g,b]=t.useState({x:0,y:0}),f=t.useCallback(e=>{if(o){const i=Math.min(Math.max(0,e.clientX-g.x),window.innerWidth-(n.boundaryPadding?.right??100)),h=Math.min(Math.max(0,e.clientY-g.y),window.innerHeight-(n.boundaryPadding?.bottom??40));l({x:i,y:h})}},[o,g,n.boundaryPadding]),p=t.useCallback(e=>{if(o){const i=Math.sqrt(Math.pow(e.clientX-a.x,2)+Math.pow(e.clientY-a.y,2)),h=Date.now()-d;i<5&&h<200&&n.onClick?.()}r(!1)},[o,a,d,n.onClick]);return t.useEffect(()=>(o&&(document.addEventListener("mousemove",f),document.addEventListener("mouseup",p)),()=>{document.removeEventListener("mousemove",f),document.removeEventListener("mouseup",p)}),[o,f,p]),{position:u,isDragging:o,handleMouseDown:e=>{e.preventDefault(),r(!0),c(Date.now()),m({x:e.clientX,y:e.clientY});const i=e.currentTarget.getBoundingClientRect();b({x:e.clientX-i.left,y:e.clientY-i.top})}}}const C=w.lazy(()=>E(()=>import("./workflowChat-CnVi6gIQ.js"),__vite__mapDeps([0,1,2,3,4,5])).then(n=>({default:n.default})));function M(){const n=t.useRef(null),[u,l]=t.useState(!1),[o,r]=t.useState(!1),{position:a,isDragging:m,handleMouseDown:d}=_({boundaryPadding:{right:100,bottom:40},onClick:()=>l(!0)});return t.useEffect(()=>{const c=()=>{l(!0),r(!0)};return window.addEventListener(x.EXPLAIN_NODE,c),()=>window.removeEventListener(x.EXPLAIN_NODE,c)},[]),a?s.jsx(v,{children:s.jsx("div",{ref:n,children:s.jsxs("div",{className:"fixed",style:{zIndex:y,left:a.x,top:a.y,cursor:m?"grabbing":"grab"},children:[s.jsxs("button",{onMouseDown:d,className:"px-2 py-1 border border-gray-300 rounded-md bg-white text-black hover:bg-blue-300 hover:font-bold transition-colors select-none w-40 flex items-center space-x-1",style:{marginLeft:"-40px"},children:[s.jsx("img",{src:D,alt:"ComfyUI-Copilot Logo",className:"h-8 w-8"}),s.jsx("span",{className:"text-[14px] font-medium text-gray-800",children:"ComfyUI-Copilot"})]}),s.jsx(w.Suspense,{fallback:s.jsx("div",{children:"Loading..."}),children:s.jsx(C,{onClose:()=>{l(!1),r(!1)},visible:u,triggerUsage:o,onUsageTriggered:()=>r(!1)})})]})})}):null}const I=Object.freeze(Object.defineProperty({__proto__:null,default:M},Symbol.toStringTag,{value:"Module"}));export{I as A,D as l};
+6
View File
@@ -0,0 +1,6 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-C0sNrgv8.js","copilot_web/input.js","copilot_web/vendor-markdown-Dek94WS0.js","copilot_web/vendor-react-V04_Axys.js","copilot_web/message-components-D3rPBq8S.js","copilot_web/assets/input-CbVoR7-h.css"].map(path => {
const apiBase = window.comfyAPI?.api?.api?.api_base;
const prefix = apiBase ? `${apiBase.substring(1)}/` : '';
return `${prefix}${path}`;
}))))=>i.map(i=>d[i]);
import{_ as E}from"./input.js";import{j as s}from"./vendor-markdown-Dek94WS0.js";import{r as n,R as w}from"./vendor-react-V04_Axys.js";import{C as v}from"./message-components-D3rPBq8S.js";const y=9999999,x={EXPLAIN_NODE:"copilot:explain-node"},D="/copilot_web/assets/logo-BTZhX0BN.png";function _(e={}){const[u,l]=n.useState({x:e.initialX??window.innerWidth-160,y:e.initialY??20}),[o,r]=n.useState(!1),[a,m]=n.useState({x:0,y:0}),[d,c]=n.useState(0),[g,b]=n.useState({x:0,y:0}),f=n.useCallback(t=>{if(o){const i=Math.min(Math.max(0,t.clientX-g.x),window.innerWidth-(e.boundaryPadding?.right??100)),p=Math.min(Math.max(0,t.clientY-g.y),window.innerHeight-(e.boundaryPadding?.bottom??40));l({x:i,y:p})}},[o,g,e.boundaryPadding]),h=n.useCallback(t=>{if(o){const i=Math.sqrt(Math.pow(t.clientX-a.x,2)+Math.pow(t.clientY-a.y,2)),p=Date.now()-d;i<5&&p<200&&e.onClick?.()}r(!1)},[o,a,d,e.onClick]);return n.useEffect(()=>(o&&(document.addEventListener("mousemove",f),document.addEventListener("mouseup",h)),()=>{document.removeEventListener("mousemove",f),document.removeEventListener("mouseup",h)}),[o,f,h]),{position:u,isDragging:o,handleMouseDown:t=>{t.preventDefault(),r(!0),c(Date.now()),m({x:t.clientX,y:t.clientY});const i=t.currentTarget.getBoundingClientRect();b({x:t.clientX-i.left,y:t.clientY-i.top})}}}const C=w.lazy(()=>E(()=>import("./workflowChat-C0sNrgv8.js").then(e=>e.w),__vite__mapDeps([0,1,2,3,4,5])).then(e=>({default:e.default})));function M(){const e=n.useRef(null),[u,l]=n.useState(!1),[o,r]=n.useState(!1),{position:a,isDragging:m,handleMouseDown:d}=_({boundaryPadding:{right:100,bottom:40},onClick:()=>l(!0)});return n.useEffect(()=>{const c=()=>{l(!0),r(!0)};return window.addEventListener(x.EXPLAIN_NODE,c),()=>window.removeEventListener(x.EXPLAIN_NODE,c)},[]),a?s.jsx(v,{children:s.jsx("div",{ref:e,children:s.jsxs("div",{className:"fixed",style:{zIndex:y,left:a.x,top:a.y,cursor:m?"grabbing":"grab"},children:[s.jsxs("button",{onMouseDown:d,className:"px-2 py-1 border border-gray-300 rounded-md bg-white text-black hover:bg-blue-300 hover:font-bold transition-colors select-none w-40 flex items-center space-x-1",style:{marginLeft:"-40px"},children:[s.jsx("img",{src:D,alt:"ComfyUI-Copilot Logo",className:"h-8 w-8"}),s.jsx("span",{className:"text-[14px] font-medium text-gray-800",children:"ComfyUI-Copilot"})]}),s.jsx(w.Suspense,{fallback:s.jsx("div",{children:"Loading..."}),children:s.jsx(C,{onClose:()=>{l(!1),r(!1)},visible:u,triggerUsage:o,onUsageTriggered:()=>r(!1)})})]})})}):null}const I=Object.freeze(Object.defineProperty({__proto__:null,default:M},Symbol.toStringTag,{value:"Module"}));export{I as A,D as l};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{j as t}from"./vendor-markdown-Dek94WS0.js";import{W as f,g as u,a as p}from"./message-components-B6pkQvDp.js";import{r as N}from"./vendor-react-V04_Axys.js";function z({content:v,name:C="Assistant",avatar:_,latestInput:y,installedNodes:k,onAddMessage:g}){const h=JSON.parse(v),w=h.ext?.find(e=>e.type==="workflow")?.data||[],[i,d]=N.useState({}),b=async e=>{if(!e.id){console.error("No workflow id provided");return}const r=String(e.id);if(!i[r]){d(o=>({...o,[r]:!0})),f.trackEvent({event_type:"workflow_accept",message_type:"workflow",message_id:h.message_id,data:{workflow_id:e.id,workflow_name:e.name}});try{const o=await f.getOptimizedWorkflow(e.id,y);if(o.workflow){const n=new Set;for(const s of o.workflow.nodes)n.add(s.type);const a=Array.from(n).filter(s=>!k.includes(s));if(console.log("[WorkflowOption] Missing node types:",a),a.length>0){try{console.log("[WorkflowOption] Fetching info for missing nodes");const s=await f.batchGetNodeInfo(a);console.log("[WorkflowOption] Received node infos:",s);const c={text:"",ext:[{type:"node_install_guide",data:s.map(l=>({name:l.name,repository_url:l.github_url}))}]},m={id:u(),role:"ai",content:JSON.stringify(c),format:"markdown",name:"Assistant",metadata:{pendingWorkflow:o.workflow,optimizedParams:o.optimized_params}};g?.(m)}catch(s){console.error("[WorkflowOption] Error fetching node info:",s),alert("Error checking required nodes. Please try again.")}finally{d(s=>({...s,[r]:!1}))}return}j(o.workflow,o.optimized_params)}}catch(o){console.error("Failed to optimize workflow:",o),alert("Failed to optimize workflow. Please try again.")}finally{d(o=>({...o,[r]:!1}))}}},j=(e,r)=>{p.loadGraphData(e);for(const[n,a,s,c,m]of r){const l=p.graph._nodes_by_id[n].widgets;for(const x of l)x.name===c&&(x.value=m)}p.graph.setDirtyCanvas(!1,!0);const o={id:u(),role:"tool",content:JSON.stringify({text:"The workflow has been successfully loaded to the canvas",ext:[]}),format:"markdown",name:"Assistant"};g?.(o)};return t.jsx("div",{className:"space-y-3",children:w.length>0&&t.jsx("div",{className:"flex flex-col space-y-4",children:w.map((e,r)=>{const o=e.id?String(e.id):"";return t.jsxs("div",{className:"flex items-center gap-4 p-4 rounded-lg border border-gray-200 hover:bg-gray-50",children:[e.image&&t.jsx("img",{src:e.image,alt:e.name,className:"w-14 h-14 object-cover rounded-lg",onError:n=>{const a=n.target;a.onerror=null,a.src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56' viewBox='0 0 56 56' fill='none'%3E%3Crect width='56' height='56' fill='%23F3F4F6'/%3E%3Cpath d='M28 28C30.2091 28 32 26.2091 32 24C32 21.7909 30.2091 20 28 20C25.7909 20 24 21.7909 24 24C24 26.2091 25.7909 28 28 28Z' fill='%239CA3AF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M18.7253 37.6307C19.8278 35.1533 22.6897 33.6 26 33.6H30C33.3103 33.6 36.1722 35.1533 37.2747 37.6307C37.6419 38.4561 37.0611 39.2 36.1694 39.2H19.8306C18.9389 39.2 18.3581 38.4561 18.7253 37.6307Z' fill='%239CA3AF'/%3E%3C/svg%3E"}}),t.jsxs("div",{className:"flex-1 break-words flex flex-col h-[4.5rem] justify-between",children:[t.jsx("div",{children:t.jsx("h3",{className:"font-medium text-sm line-clamp-2 h-10 overflow-hidden",children:e.name})}),t.jsxs("div",{className:"flex justify-between items-center mt-1",children:[e.description&&t.jsxs("div",{className:"relative group",children:[t.jsx("div",{className:"w-5 h-5 flex items-center justify-center text-gray-500 cursor-help",children:t.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor",className:"w-4 h-4",children:t.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"})})}),t.jsx("div",{className:"absolute bottom-full left-0 mb-2 hidden group-hover:block",children:t.jsx("div",{className:"bg-gray-900 text-white text-xs rounded-md py-2 px-3 min-w-[400px] whitespace-normal break-words",children:e.description})})]}),t.jsx("button",{onClick:()=>b(e),disabled:i[o],className:`px-3 py-1.5 ${i[o]?"bg-gray-400 cursor-not-allowed":"bg-blue-500 hover:bg-blue-600"} text-white rounded-md transition-colors text-xs`,children:i[o]?"Loading...":"Accept"})]})]})]},r)})})})}export{z as WorkflowOption};
import{j as t}from"./vendor-markdown-Dek94WS0.js";import{W as f,g as u,a as p}from"./message-components-D3rPBq8S.js";import{r as N}from"./vendor-react-V04_Axys.js";function z({content:v,name:C="Assistant",avatar:_,latestInput:y,installedNodes:k,onAddMessage:g}){const h=JSON.parse(v),w=h.ext?.find(e=>e.type==="workflow")?.data||[],[i,d]=N.useState({}),b=async e=>{if(!e.id){console.error("No workflow id provided");return}const r=String(e.id);if(!i[r]){d(o=>({...o,[r]:!0})),f.trackEvent({event_type:"workflow_accept",message_type:"workflow",message_id:h.message_id,data:{workflow_id:e.id,workflow_name:e.name}});try{const o=await f.getOptimizedWorkflow(e.id,y);if(o.workflow){const n=new Set;for(const s of o.workflow.nodes)n.add(s.type);const a=Array.from(n).filter(s=>!k.includes(s));if(console.log("[WorkflowOption] Missing node types:",a),a.length>0){try{console.log("[WorkflowOption] Fetching info for missing nodes");const s=await f.batchGetNodeInfo(a);console.log("[WorkflowOption] Received node infos:",s);const c={text:"",ext:[{type:"node_install_guide",data:s.map(l=>({name:l.name,repository_url:l.github_url}))}]},m={id:u(),role:"ai",content:JSON.stringify(c),format:"markdown",name:"Assistant",metadata:{pendingWorkflow:o.workflow,optimizedParams:o.optimized_params}};g?.(m)}catch(s){console.error("[WorkflowOption] Error fetching node info:",s),alert("Error checking required nodes. Please try again.")}finally{d(s=>({...s,[r]:!1}))}return}j(o.workflow,o.optimized_params)}}catch(o){console.error("Failed to optimize workflow:",o),alert("Failed to optimize workflow. Please try again.")}finally{d(o=>({...o,[r]:!1}))}}},j=(e,r)=>{p.loadGraphData(e);for(const[n,a,s,c,m]of r){const l=p.graph._nodes_by_id[n].widgets;for(const x of l)x.name===c&&(x.value=m)}p.graph.setDirtyCanvas(!1,!0);const o={id:u(),role:"tool",content:JSON.stringify({text:"The workflow has been successfully loaded to the canvas",ext:[]}),format:"markdown",name:"Assistant"};g?.(o)};return t.jsx("div",{className:"space-y-3",children:w.length>0&&t.jsx("div",{className:"flex flex-col space-y-4",children:w.map((e,r)=>{const o=e.id?String(e.id):"";return t.jsxs("div",{className:"flex items-center gap-4 p-4 rounded-lg border border-gray-200 hover:bg-gray-50",children:[e.image&&t.jsx("img",{src:e.image,alt:e.name,className:"w-14 h-14 object-cover rounded-lg",onError:n=>{const a=n.target;a.onerror=null,a.src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56' viewBox='0 0 56 56' fill='none'%3E%3Crect width='56' height='56' fill='%23F3F4F6'/%3E%3Cpath d='M28 28C30.2091 28 32 26.2091 32 24C32 21.7909 30.2091 20 28 20C25.7909 20 24 21.7909 24 24C24 26.2091 25.7909 28 28 28Z' fill='%239CA3AF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M18.7253 37.6307C19.8278 35.1533 22.6897 33.6 26 33.6H30C33.3103 33.6 36.1722 35.1533 37.2747 37.6307C37.6419 38.4561 37.0611 39.2 36.1694 39.2H19.8306C18.9389 39.2 18.3581 38.4561 18.7253 37.6307Z' fill='%239CA3AF'/%3E%3C/svg%3E"}}),t.jsxs("div",{className:"flex-1 break-words flex flex-col h-[4.5rem] justify-between",children:[t.jsx("div",{children:t.jsx("h3",{className:"font-medium text-sm line-clamp-2 h-10 overflow-hidden",children:e.name})}),t.jsxs("div",{className:"flex justify-between items-center mt-1",children:[e.description&&t.jsxs("div",{className:"relative group",children:[t.jsx("div",{className:"w-5 h-5 flex items-center justify-center text-gray-500 cursor-help",children:t.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor",className:"w-4 h-4",children:t.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"})})}),t.jsx("div",{className:"absolute bottom-full left-0 mb-2 hidden group-hover:block",children:t.jsx("div",{className:"bg-gray-900 text-white text-xs rounded-md py-2 px-3 min-w-[400px] whitespace-normal break-words",children:e.description})})]}),t.jsx("button",{onClick:()=>b(e),disabled:i[o],className:`px-3 py-1.5 ${i[o]?"bg-gray-400 cursor-not-allowed":"bg-blue-500 hover:bg-blue-600"} text-white rounded-md transition-colors text-xs`,children:i[o]?"Loading...":"Accept"})]})]})]},r)})})})}export{z as WorkflowOption};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,2 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/App-BjVABi7E.js","copilot_web/vendor-markdown-Dek94WS0.js","copilot_web/vendor-react-V04_Axys.js","copilot_web/message-components-B6pkQvDp.js"])))=>i.map(i=>d[i]);
import{j as u}from"./vendor-markdown-Dek94WS0.js";import{a as v,R as p}from"./vendor-react-V04_Axys.js";import{w}from"./message-components-B6pkQvDp.js";const R="modulepreload",b=function(e){return"/"+e},m={},P=function(a,s,S){let d=Promise.resolve();if(s&&s.length>0){document.getElementsByTagName("link");const r=document.querySelector("meta[property=csp-nonce]"),t=r?.nonce||r?.getAttribute("nonce");d=Promise.allSettled(s.map(o=>{if(o=b(o),o in m)return;m[o]=!0;const c=o.endsWith(".css"),h=c?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${o}"]${h}`))return;const n=document.createElement("link");if(n.rel=c?"stylesheet":R,c||(n.as="script"),n.crossOrigin="",n.href=o,t&&n.setAttribute("nonce",t),document.head.appendChild(n),c)return new Promise((y,E)=>{n.addEventListener("load",y),n.addEventListener("error",()=>E(new Error(`Unable to preload CSS for ${o}`)))})}))}function l(r){const t=new Event("vite:preloadError",{cancelable:!0});if(t.payload=r,window.dispatchEvent(t),!t.defaultPrevented)throw r}return d.then(r=>{for(const t of r||[])t.status==="rejected"&&l(t.reason);return a().catch(l)})};var i={},f=v;i.createRoot=f.createRoot,i.hydrateRoot=f.hydrateRoot;const _=p.lazy(()=>P(async()=>{const{default:e}=await import("./App-BjVABi7E.js").then(a=>a.A);return{default:e}},__vite__mapDeps([0,1,2,3])).then(({default:e})=>({default:e})));function A(){return new Promise(e=>{if(document.body)return e(document.body);document.addEventListener("DOMContentLoaded",()=>{e(document.body)})})}A().then(()=>w()).then(()=>{const e=document.createElement("div");e.id="comfyui-copilot-plugin",document.body.append(e),i.createRoot(e).render(u.jsx(p.StrictMode,{children:u.jsx(_,{})}))});export{P as _};
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/App-CzPQ6Sqn.js","copilot_web/vendor-markdown-Dek94WS0.js","copilot_web/vendor-react-V04_Axys.js","copilot_web/message-components-D3rPBq8S.js"])))=>i.map(i=>d[i]);
import{j as u}from"./vendor-markdown-Dek94WS0.js";import{a as v,R as p}from"./vendor-react-V04_Axys.js";import{w}from"./message-components-D3rPBq8S.js";const R="modulepreload",b=function(e){return"/"+e},m={},P=function(a,s,S){let d=Promise.resolve();if(s&&s.length>0){document.getElementsByTagName("link");const r=document.querySelector("meta[property=csp-nonce]"),t=r?.nonce||r?.getAttribute("nonce");d=Promise.allSettled(s.map(o=>{if(o=b(o),o in m)return;m[o]=!0;const c=o.endsWith(".css"),h=c?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${o}"]${h}`))return;const n=document.createElement("link");if(n.rel=c?"stylesheet":R,c||(n.as="script"),n.crossOrigin="",n.href=o,t&&n.setAttribute("nonce",t),document.head.appendChild(n),c)return new Promise((y,E)=>{n.addEventListener("load",y),n.addEventListener("error",()=>E(new Error(`Unable to preload CSS for ${o}`)))})}))}function l(r){const t=new Event("vite:preloadError",{cancelable:!0});if(t.payload=r,window.dispatchEvent(t),!t.defaultPrevented)throw r}return d.then(r=>{for(const t of r||[])t.status==="rejected"&&l(t.reason);return a().catch(l)})};var i={},f=v;i.createRoot=f.createRoot,i.hydrateRoot=f.hydrateRoot;const _=p.lazy(()=>P(async()=>{const{default:e}=await import("./App-CzPQ6Sqn.js").then(a=>a.A);return{default:e}},__vite__mapDeps([0,1,2,3])).then(({default:e})=>({default:e})));function A(){return new Promise(e=>{if(document.body)return e(document.body);document.addEventListener("DOMContentLoaded",()=>{e(document.body)})})}A().then(()=>w()).then(()=>{const e=document.createElement("div");e.id="comfyui-copilot-plugin",document.body.append(e),i.createRoot(e).render(u.jsx(p.StrictMode,{children:u.jsx(_,{})}))});export{P as _};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI-Copilot"
description = "Your Intelligent Assistant for Comfy-UI."
version = "1.0.6"
version = "1.1.0"
license = {file = "LICENSE"}
[project.urls]
+1
View File
@@ -10,6 +10,7 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"setupGithooks": "node ../scripts/setupGitHooks",
"build:pre": "npx tailwindcss -i ./src/scoped-tailwind.css -o ./src/output.css && VITE_API_BASE_URL=https://comfyui-copilot-server-pre.onrender.com vite build && node scripts/post-build.js",
"build:prod": "npx tailwindcss -i ./src/scoped-tailwind.css -o ./src/output.css && VITE_API_BASE_URL=https://comfyui-copilot-server.onrender.com vite build && node scripts/post-build.js"
},
"dependencies": {
+104
View File
@@ -1,3 +1,11 @@
/*
* @Author: ai-business-hql ai.bussiness.hql@gmail.com
* @Date: 2025-02-17 20:53:45
* @LastEditors: ai-business-hql ai.bussiness.hql@gmail.com
* @LastEditTime: 2025-04-02 11:06:22
* @FilePath: /comfyui_copilot/ui/src/apis/comfyApiCustom.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
@@ -17,3 +25,99 @@ export async function getInstalledNodes(): Promise<string[]> {
const objectInfos = await getObjectInfo();
return Object.keys(objectInfos);
}
export async function runPrompt(json_data: any): Promise<any> {
const response = await api.fetchApi("/prompt", {
method: "POST",
body: JSON.stringify(json_data),
});
return await response.json();
}
/**
* Clear the prompt queue or delete specific queue items
*
* @param options Configuration options
* @param options.clear If true, clears the entire queue
* @param options.delete Array of prompt IDs to delete from the queue
* @returns Promise with the response
*
* @example
* // Clear the entire queue
* await manageQueue({ clear: true });
*
* // Delete specific prompts from the queue
* await manageQueue({ delete: ["prompt-123", "prompt-456"] });
*
* // Both clear the queue and delete specific prompts
* await manageQueue({ clear: true, delete: ["prompt-789"] });
*/
export async function manageQueue(options: {
clear?: boolean;
delete?: string[];
}): Promise<Response> {
try {
const response = await api.fetchApi("/queue", {
method: "POST",
body: JSON.stringify(options),
});
if (!response.ok) {
throw new Error(`Failed to manage queue: ${response.status} ${response.statusText}`);
}
return response;
} catch (error) {
console.error("Error managing queue:", error);
throw error;
}
}
/**
* Interrupts the current processing/generation
*
* This function sends a request to the server to stop the current
* processing operation. It's useful for canceling ongoing image generations.
*
* @returns Promise with the response
*
* @example
* // Interrupt the current generation process
* await interruptProcessing();
*/
export async function interruptProcessing(): Promise<Response> {
try {
const response = await api.fetchApi("/interrupt", {
method: "POST",
});
if (!response.ok) {
throw new Error(`Failed to interrupt processing: ${response.status} ${response.statusText}`);
}
return response;
} catch (error) {
console.error("Error interrupting processing:", error);
throw error;
}
}
export async function getHistory(promptId: string): Promise<any> {
try {
const response = await api.fetchApi(`/history/${promptId}`, {
method: "GET"
});
if (!response.ok) {
throw new Error(`Failed to fetch history: ${response.status} ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error("Error fetching history:", error);
throw error;
}
}
+54
View File
@@ -389,6 +389,60 @@ export namespace WorkflowChatAPI {
return '';
}
}
export async function generateSDPrompts(text: string): Promise<string[]> {
const maxRetries = 3;
let retryCount = 0;
let lastError: any;
while (retryCount < maxRetries) {
try {
const apiKey = getApiKey();
const browserLanguage = getBrowserLanguage();
// Prepare headers
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'accept': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'trace-id': generateUUID(),
'Accept-Language': browserLanguage,
};
const response = await fetch(`${BASE_URL}/api/param_debug/generate_sd_prompts`, {
method: 'POST',
headers,
body: JSON.stringify({
text: text
}),
});
const result = await response.json();
if (!result.success) {
const message = result.message || 'Failed to generate SD prompts';
throw new Error(message);
}
return result.data as string[];
} catch (error) {
lastError = error;
retryCount++;
if (retryCount >= maxRetries) {
console.error(`Error generating SD prompts after ${maxRetries} attempts:`, error);
throw error;
}
console.warn(`Attempt ${retryCount} failed, retrying... Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
// Wait with exponential backoff before retrying (500ms, 1000ms, 2000ms)
await new Promise(resolve => setTimeout(resolve, 500 * Math.pow(2, retryCount - 1)));
}
}
// This should never be reached due to the throw in the loop, but TypeScript needs it
throw lastError;
}
}
+12 -5
View File
@@ -19,9 +19,16 @@ interface ChatHeaderProps {
onClear?: () => void;
hasMessages: boolean;
onHeightResize?: (deltaY: number) => void;
title?: string;
}
export function ChatHeader({ onClose, onClear, hasMessages, onHeightResize }: ChatHeaderProps) {
export function ChatHeader({
onClose,
onClear,
hasMessages,
onHeightResize,
title = "ComfyUI-Copilot"
}: ChatHeaderProps) {
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false);
const [isResizing, setIsResizing] = useState(false);
@@ -76,9 +83,9 @@ export function ChatHeader({ onClose, onClear, hasMessages, onHeightResize }: Ch
<img
src={logoImage}
alt="ComfyUI-Copilot Logo"
className="h-12 w-12 -ml-2"
className="h-10 w-10 -ml-1"
/>
<h3 className="text-[16px] font-medium text-gray-800">ComfyUI-Copilot</h3>
<h3 className="text-[16px] font-medium text-gray-800">{title}</h3>
<button
onClick={handleApiKeyClick}
className="p-1 bg-white border-none hover:bg-gray-100 rounded text-gray-500"
@@ -88,8 +95,8 @@ export function ChatHeader({ onClose, onClear, hasMessages, onHeightResize }: Ch
</div>
<div className="flex items-center gap-1">
<button
className="inline-flex bg-white border-none items-center justify-center rounded-md p-2
text-gray-500 hover:bg-gray-100"
className={`inline-flex bg-white border-none items-center justify-center rounded-md p-2
${hasMessages ? 'text-gray-500 hover:bg-gray-100' : 'text-gray-300 cursor-not-allowed'}`}
disabled={!hasMessages}
onClick={onClear}>
<TrashIcon className="h-5 w-5" />
+26 -7
View File
@@ -27,6 +27,25 @@ export interface UploadedImage {
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const SUPPORTED_FORMATS = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const models_conf = [
{
"name": "gpt-4o",
"image_enable": true
},
{
"name": "gpt-4o-mini",
"image_enable": true
},
{
"name": "qwen-plus",
"image_enable": false
},
{
"name": "DeepSeek-V3",
"image_enable": false
}
]
export function ChatInput({
input,
loading,
@@ -140,20 +159,20 @@ export function ChatInput({
focus:border-transparent hover:bg-gray-50
transition-colors border-0"
>
<option value="gpt-4o-mini">gpt-4o-mini</option>
<option value="gpt-4o">gpt-4o</option>
<option value="qwen-plus">qwen-plus</option>
<option value="DeepSeek-V3">DeepSeek-V3</option>
{models_conf.map((model) => (
<option value={model.name} key={model.name}>{model.name}</option>
))}
</select>
{/* Upload image button */}
<button
type="button"
onClick={() => setShowUploadModal(true)}
className="p-1.5 text-gray-500 bg-white border-none
disabled={!models_conf.find(model => model.name === selectedModel)?.image_enable}
className={`p-1.5 text-gray-500 bg-white border-none
hover:bg-gray-100 hover:text-gray-600
transition-all duration-200 outline-none">
transition-all duration-200 outline-none
${!models_conf.find(model => model.name === selectedModel)?.image_enable ? 'opacity-50 cursor-not-allowed' : ''}`}>
<ImageIcon className="h-4 w-4" />
</button>
</div>
+5 -5
View File
@@ -2,7 +2,7 @@
* @Author: 晴知 qingli.hql@alibaba-inc.com
* @Date: 2025-02-17 20:53:45
* @LastEditors: ai-business-hql ai.bussiness.hql@gmail.com
* @LastEditTime: 2025-02-27 19:22:25
* @LastEditTime: 2025-03-20 18:55:32
* @FilePath: /comfyui_copilot/ui/src/components/chat/SelectedNodeInfo.tsx
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@@ -12,7 +12,7 @@
import { app } from "../../utils/comfyapp";
interface SelectedNodeInfoProps {
nodeInfo: any;
nodeInfo: any[];
onSendWithIntent: (intent: string, ext?: any) => void;
loading: boolean;
onSendWithContent: (text: string) => void;
@@ -53,19 +53,19 @@ export function SelectedNodeInfo({ nodeInfo, onSendWithIntent, loading, onSendWi
return (
<div className="mb-3 p-3 rounded-md bg-gray-50 border border-gray-200">
<div className="text-sm text-gray-700">
<p>Selected node: {nodeInfo.type}</p>
<p>Selected node: {nodeInfo[0].type}</p>
<div className="flex gap-2 mt-2">
<button
className="px-3 py-1 text-xs rounded-md bg-blue-50
text-blue-700 hover:bg-blue-100"
onClick={() => onSendWithContent(`Reply in ${navigator.language} language: How does the ${nodeInfo.type} node work? I need its official usage guide.`)}
onClick={() => onSendWithContent(`Reply in ${navigator.language} language: How does the ${nodeInfo[0].type} node work? I need its official usage guide.`)}
disabled={loading}>
Usage
</button>
<button
className="px-3 py-1 text-xs rounded-md bg-green-100
text-green-700 hover:bg-green-200"
onClick={() => onSendWithContent(`Reply in ${navigator.language} language: Show me the technical specifications for the ${nodeInfo.type} node's inputs and outputs.`)}
onClick={() => onSendWithContent(`Reply in ${navigator.language} language: Show me the technical specifications for the ${nodeInfo[0].type} node's inputs and outputs.`)}
disabled={loading}>
Parameters
</button>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,142 @@
import React from 'react';
interface AIWritingModalProps {
visible: boolean;
aiWritingModalText: string;
setAiWritingModalText: React.Dispatch<React.SetStateAction<string>>;
aiGeneratedTexts: string[];
aiSelectedTexts: {[key: string]: boolean};
aiWritingLoading: boolean;
aiWritingError: string | null;
handleAiWriting: () => void;
toggleTextSelection: (textKey: string) => void;
addSelectedTexts: () => void;
onClose: () => void;
}
export const AIWritingModal: React.FC<AIWritingModalProps> = ({
visible,
aiWritingModalText,
setAiWritingModalText,
aiGeneratedTexts,
aiSelectedTexts,
aiWritingLoading,
aiWritingError,
handleAiWriting,
toggleTextSelection,
addSelectedTexts,
onClose
}) => {
if (!visible) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-lg w-full max-w-2xl max-h-[90vh] flex flex-col">
<div className="p-4 border-b flex justify-between items-center">
<h3 className="text-lg font-medium">AI Writing Assistant</h3>
<button
className="text-gray-400 hover:text-gray-600"
onClick={onClose}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-4 overflow-y-auto flex-1">
<div className="mb-4">
<p className="text-sm text-gray-600 mb-2">Enter text and AI will generate variations</p>
<textarea
className="w-full border border-gray-300 rounded-md p-2 text-sm"
rows={3}
placeholder="Enter some text here..."
value={aiWritingModalText}
onChange={(e) => setAiWritingModalText(e.target.value)}
/>
<button
className="mt-2 px-4 py-2 bg-blue-100 text-blue-700 rounded-md text-sm flex items-center hover:bg-blue-200 transition-colors"
onClick={handleAiWriting}
disabled={aiWritingLoading}
>
{aiWritingLoading ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-blue-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Generating...
</>
) : (
<>Generate</>
)}
</button>
</div>
{aiWritingError && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
{aiWritingError}
</div>
)}
{aiGeneratedTexts.length > 0 && (
<div className="mb-4">
<p className="text-sm font-medium mb-2">Generated variations (select items to add):</p>
<div className="space-y-2 max-h-[40vh] overflow-y-auto p-2">
{aiGeneratedTexts.map((text, idx) => (
<div key={idx} className="flex items-start border border-gray-200 rounded-md p-2">
<div className="mr-2 mt-1">
<div className="relative">
<input
type="checkbox"
id={`text${idx+1}`}
checked={!!aiSelectedTexts[`text${idx+1}`]}
onChange={() => toggleTextSelection(`text${idx+1}`)}
className="sr-only" /* Hide the actual checkbox but keep it functional */
/>
<div
className={`h-4 w-4 rounded border ${
aiSelectedTexts[`text${idx+1}`]
? 'bg-blue-600 border-blue-600'
: 'bg-white border-gray-300'
} flex items-center justify-center`}
onClick={() => toggleTextSelection(`text${idx+1}`)}
>
{aiSelectedTexts[`text${idx+1}`] && (
<svg className="h-3 w-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
</div>
</div>
<label htmlFor={`text${idx+1}`} className="text-sm text-gray-700 cursor-pointer flex-1">
{text}
</label>
</div>
))}
</div>
</div>
)}
</div>
<div className="p-4 border-t flex justify-end flex-shrink-0">
<button
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md text-sm mr-2 hover:bg-gray-200 transition-colors"
onClick={onClose}
>
Cancel
</button>
<button
className="px-4 py-2 bg-blue-500 text-white rounded-md text-sm hover:bg-blue-600 transition-colors"
onClick={addSelectedTexts}
disabled={Object.values(aiSelectedTexts).every(v => !v)}
>
Add Selected
</button>
</div>
</div>
</div>
);
};
@@ -0,0 +1,37 @@
import React from 'react';
interface ImageModalProps {
visible: boolean;
imageUrl: string;
onClose: (event?: React.MouseEvent) => void;
}
export const ImageModal: React.FC<ImageModalProps> = ({
visible,
imageUrl,
onClose
}) => {
if (!visible) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50" onClick={onClose}>
<div className="bg-white rounded-lg p-2 max-w-4xl max-h-[90vh] relative" onClick={(e) => e.stopPropagation()}>
<button
className="absolute top-2 right-2 text-gray-700 hover:text-gray-900 rounded-full hover:bg-gray-200 p-1"
onClick={onClose}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="overflow-auto max-h-[calc(90vh-4rem)]">
<img
src={imageUrl}
alt="Enlarged image"
className="max-w-full h-auto"
/>
</div>
</div>
</div>
);
};
@@ -0,0 +1,690 @@
import React, { useEffect } from 'react';
interface ConfigureParameterScreenProps {
selectedNodes: any[];
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}};
selectedParams: {[key: string]: boolean};
updateParamTestValues: (nodeId: string, paramName: string, values: any[]) => void;
toggleDropdown: (nodeId: string, paramName: string, event: React.MouseEvent) => void;
openDropdowns: {[key: string]: boolean | {isOpen: boolean, x: number, y: number}};
handleTestValueSelect: (nodeId: string, paramName: string, value: any, event?: React.MouseEvent) => void;
searchTerms: {[key: string]: string};
handleSearch: (nodeId: string, paramName: string, term: string) => void;
handleSelectAll: (nodeId: string, paramName: string, values: any[]) => void;
handleNext: (event?: React.MouseEvent) => void;
handlePrevious: (event?: React.MouseEvent) => void;
handleClose: (event?: React.MouseEvent, nodeIndex?: number) => void;
inputValues: {[nodeId_paramName: string]: {min?: string, max?: string, step?: string}};
setInputValues: React.Dispatch<React.SetStateAction<{[nodeId_paramName: string]: {min?: string, max?: string, step?: string}}>>;
textInputs: {[nodeId_paramName: string]: string[]};
handleTextInputChange: (nodeId: string, paramName: string, index: number, value: string) => void;
handleAddTextInput: (nodeId: string, paramName: string) => void;
handleRemoveTextInput: (nodeId: string, paramName: string, index: number) => void;
openAiWritingModal: (nodeId: string, paramName: string) => void;
}
export const ConfigureParameterScreen: React.FC<ConfigureParameterScreenProps> = ({
selectedNodes,
paramTestValues,
selectedParams,
updateParamTestValues,
toggleDropdown,
openDropdowns,
handleTestValueSelect,
searchTerms,
handleSearch,
handleSelectAll,
handleNext,
handlePrevious,
handleClose,
inputValues,
setInputValues,
textInputs,
handleTextInputChange,
handleAddTextInput,
handleRemoveTextInput,
openAiWritingModal
}) => {
// Function to generate numeric test values
const generateNumericTestValues = (min: number, max: number, step: number, precision: number = 0) => {
// Ensure all parameters have valid values
min = isNaN(min) ? 0 : min;
max = isNaN(max) ? 100 : max;
step = isNaN(step) || step <= 0 ? 1 : step;
// Ensure max >= min
if (max < min) {
max = min;
}
// Calculate count based on the actual step
const count = Math.floor((max - min) / step) + 1;
if (count <= 1) return [min];
// Generate values based on the actual step
const values = [];
let current = min;
// Generate all values between min and max using step
while (current <= max) {
// Apply precision based on the parameter
const factor = Math.pow(10, precision);
const roundedValue = Math.round(current * factor) / factor;
values.push(roundedValue);
current += step;
}
return values;
};
// Initialize default values for numeric parameters when component mounts
useEffect(() => {
if (!selectedNodes || selectedNodes.length === 0) return;
// For each selected node
selectedNodes.forEach(node => {
const nodeId = node.id.toString();
const widgets = node.widgets || {};
const nodeWidgets = Object.values(widgets);
// For each widget in the node
nodeWidgets.forEach((widget: any) => {
const paramName = widget.name;
// Only process selected parameters
if (!selectedParams[paramName]) return;
// Check if we already have values for this parameter
const hasValues = paramTestValues[nodeId]?.[paramName]?.length > 0;
// If no values exist yet and it's a numeric parameter
if (!hasValues && widget.type === "number") {
const min = widget.options?.min || 0;
const max = widget.options?.max || 100;
const step = (widget.options?.step || 10) / 10;
const precision = widget.options?.precision || 0;
// Generate default values
const defaultValues = generateNumericTestValues(min, max, step, precision);
// Store these values in the state
if (defaultValues.length > 0) {
updateParamTestValues(nodeId, paramName, defaultValues);
// Also update the input values state
const inputKey = `${nodeId}_${paramName}`;
setInputValues(prev => ({
...prev,
[inputKey]: {
min: min.toString(),
max: max.toString(),
step: step.toString()
}
}));
}
}
// Handle text inputs - Always ensure paramTestValues are in sync with textInputs
if ((widget.type === "customtext" || widget.type.toLowerCase().includes("text"))) {
const inputKey = `${nodeId}_${paramName}`;
const currentTexts = textInputs[inputKey] || [''];
// Always update paramTestValues with current textInputs to ensure they're in sync
// This ensures text values are maintained throughout the workflow
if (JSON.stringify(currentTexts) !== JSON.stringify(paramTestValues[nodeId]?.[paramName] || [''])) {
updateParamTestValues(nodeId, paramName, currentTexts);
}
}
});
});
}, [selectedNodes, selectedParams, paramTestValues, textInputs, updateParamTestValues, setInputValues, generateNumericTestValues]);
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<div>
<h3 className="text-base font-medium text-gray-800">Parameter Options</h3>
<p className="text-xs text-gray-500">Configure the test values for each parameter</p>
</div>
<button
className="text-gray-400 hover:text-gray-600"
onClick={handleClose}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{selectedNodes.length > 0 ? (
selectedNodes.map((node, nodeIndex) => {
const widgets = node.widgets || {};
const nodeWidgets = Object.values(widgets);
const nodeId = node.id; // 获取节点ID
return (
<div key={nodeIndex} className="border rounded-md mb-4 overflow-hidden">
<div className="bg-gray-50 px-3 py-2 text-xs font-medium text-gray-700 border-b flex justify-between items-center">
<span>{node.type} (ID: {nodeId})</span>
{/* Add close button for individual node card */}
<button
className="text-gray-400 hover:text-gray-600"
onClick={(e) => handleClose(e, nodeIndex)}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-4 space-y-4">
{nodeWidgets.map((widget: any, widgetIndex: number) => {
const paramName = widget.name;
// Only show selected parameters
if (!selectedParams[paramName]) {
return null;
}
// 为每个输入字段创建唯一的键值
const inputKey = `${nodeId}_${paramName}`;
// Handle text parameter type
if (widget.type === "customtext" || widget.type.toLowerCase().includes("text")) {
const currentTexts = textInputs[inputKey] || [''];
return (
<div key={widgetIndex} className="border-b pb-3">
<div className="flex justify-between items-center mb-2">
<label className="text-xs font-medium text-gray-700">{paramName}:</label>
<button
className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs flex items-center hover:bg-blue-200 transition-colors"
onClick={() => openAiWritingModal(nodeId, paramName)}
>
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
AI Writing
</button>
</div>
<div className="space-y-2">
{currentTexts.map((text, textIndex) => (
<div key={textIndex} className="flex items-start">
<textarea
className="flex-1 border border-gray-300 rounded p-2 text-xs resize-y"
rows={2}
placeholder="Enter text..."
value={text}
onChange={(e) => handleTextInputChange(nodeId, paramName, textIndex, e.target.value)}
/>
<button
className="ml-2 text-red-500 hover:text-red-700"
onClick={() => handleRemoveTextInput(nodeId, paramName, textIndex)}
title="Remove"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
</div>
<button
className="mt-2 px-2 py-1 border border-dashed border-gray-300 rounded text-xs text-gray-600 hover:bg-gray-50 flex items-center"
onClick={() => handleAddTextInput(nodeId, paramName)}
>
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Add Text
</button>
</div>
);
}
if (widget.type === "number") {
const min = widget.options?.min || 0;
const max = widget.options?.max || 100;
const step = (widget.options?.step || 10) / 10;
const precision = widget.options?.precision || 0;
const defaultValues = generateNumericTestValues(min, max, step, precision);
// 获取当前输入状态,如果不存在则初始化
const currentInputs = inputValues[inputKey] || {
min: min.toString(),
max: max.toString(),
step: step.toString()
};
return (
<div key={widgetIndex} className="border-b pb-3">
<div className="flex justify-between items-center">
<label className="text-xs font-medium text-gray-700">{paramName}:</label>
<div className="w-4"></div>
</div>
<div className="flex space-x-2 items-center">
<label className="text-xs text-gray-600">Min</label>
<input
type="text"
className="w-12 h-6 border border-gray-300 rounded text-xs px-2"
value={currentInputs.min}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
const value = e.target.value;
// 更新输入状态
setInputValues(prev => ({
...prev,
[inputKey]: {
...prev[inputKey],
min: value
}
}));
// 只有当输入为空或有效时才更新参数值
if (value === '' || !isNaN(parseFloat(value))) {
const newMin = value === '' ? 0 : parseFloat(value);
const newMax = parseFloat(currentInputs.max || max.toString());
const newStep = parseFloat(currentInputs.step || step.toString());
const newValues = generateNumericTestValues(newMin, newMax, newStep, precision);
updateParamTestValues(nodeId, paramName, newValues);
}
}}
/>
<label className="text-xs text-gray-600">Max</label>
<input
type="text"
className="w-12 h-6 border border-gray-300 rounded text-xs px-2"
value={currentInputs.max}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
const value = e.target.value;
// 更新输入状态
setInputValues(prev => ({
...prev,
[inputKey]: {
...prev[inputKey],
max: value
}
}));
// 只有当输入为空或有效时才更新参数值
if (value === '' || !isNaN(parseFloat(value))) {
const newMin = parseFloat(currentInputs.min || min.toString());
const newMax = value === '' ? newMin : parseFloat(value);
const newStep = parseFloat(currentInputs.step || step.toString());
const newValues = generateNumericTestValues(newMin, newMax, newStep, precision);
updateParamTestValues(nodeId, paramName, newValues);
}
}}
/>
<label className="text-xs text-gray-600">Step</label>
<input
type="text"
className="w-12 h-6 border border-gray-300 rounded text-xs px-2"
value={currentInputs.step}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
const value = e.target.value;
// 更新输入状态
setInputValues(prev => ({
...prev,
[inputKey]: {
...prev[inputKey],
step: value
}
}));
// 只有当输入为空或有效时才更新参数值
if (value === '' || (!isNaN(parseFloat(value)) && parseFloat(value) > 0)) {
const newMin = parseFloat(currentInputs.min || min.toString());
const newMax = parseFloat(currentInputs.max || max.toString());
const newStep = value === '' ? 1 : parseFloat(value);
const newValues = generateNumericTestValues(newMin, newMax, newStep, precision);
updateParamTestValues(nodeId, paramName, newValues);
}
}}
/>
</div>
<div className="mt-1">
<label className="text-xs text-gray-600">Test values</label>
<div className="mt-1 flex flex-wrap gap-1">
{(() => {
const values = paramTestValues[nodeId]?.[paramName] || defaultValues;
// If 10 values or less, show all of them
if (values.length <= 10) {
return values.map((value, idx) => (
<div
key={idx}
className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-md flex items-center text-xs cursor-pointer hover:bg-blue-200"
onClick={(e) => {
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<span>{value}</span>
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
));
} else {
// Show first 5, ellipsis, and last 5
const firstFive = values.slice(0, 5);
const lastFive = values.slice(values.length - 5);
return (
<>
{firstFive.map((value, idx) => (
<div
key={`first-${idx}`}
className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-md flex items-center text-xs cursor-pointer hover:bg-blue-200"
onClick={(e) => {
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<span>{value}</span>
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
))}
{/* Ellipsis indicator */}
<div className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded-md flex items-center text-xs">
...
</div>
{lastFive.map((value, idx) => (
<div
key={`last-${idx}`}
className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-md flex items-center text-xs cursor-pointer hover:bg-blue-200"
onClick={(e) => {
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<span>{value}</span>
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
))}
</>
);
}
})()}
</div>
</div>
<div className="mt-1">
<p className="text-xs text-gray-500">Values are distributed evenly between Min and Max</p>
</div>
</div>
);
} else if (widget.type === "combo") {
const values = widget.options?.values || [];
const dropdownKey = `${nodeId}_${paramName}`;
return (
<div key={widgetIndex} className="border-b pb-3">
<div className="flex justify-between items-center">
<label className="text-xs font-medium text-gray-700">{paramName}</label>
<div className="relative dropdown-container">
<button
className="appearance-none border border-gray-300 rounded px-2 py-0.5 pr-6 text-xs flex items-center bg-white text-gray-700"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleDropdown(nodeId, paramName, e);
}}
data-dropdown={dropdownKey}
>
<span>
{(paramTestValues[nodeId]?.[paramName] || []).length > 0
? `${paramName}: ${(paramTestValues[nodeId]?.[paramName] || []).length} selected`
: `Select ${paramName}`}
</span>
<div className="ml-2">
<svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/>
</svg>
</div>
</button>
{openDropdowns[dropdownKey] && (
<div
className="absolute z-[9999] mt-1 w-64 bg-white border border-gray-300 rounded-md shadow-lg dropdown-menu"
style={{
position: 'fixed',
top: typeof openDropdowns[dropdownKey] === 'object' ?
`${(openDropdowns[dropdownKey] as {y: number}).y}px` : '0px',
left: typeof openDropdowns[dropdownKey] === 'object' ?
`${(openDropdowns[dropdownKey] as {x: number}).x}px` : '0px',
maxHeight: '300px',
overflowY: 'auto',
}}
>
{/* Search box */}
<div className="p-2 border-b sticky top-0 bg-white">
<input
type="text"
className="w-full px-2 py-0.5 text-xs border border-gray-300 rounded"
placeholder="Search..."
value={searchTerms[dropdownKey] || ''}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
handleSearch(nodeId, paramName, e.target.value);
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
autoFocus
/>
</div>
{/* Select all option */}
<div
className="px-3 py-1.5 bg-gray-50 border-b cursor-pointer text-xs flex items-center text-gray-700 hover:bg-gray-100"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const filteredValues = values.filter((value: string) =>
!searchTerms[dropdownKey] ||
value.toLowerCase().includes((searchTerms[dropdownKey] || '').toLowerCase())
);
handleSelectAll(nodeId, paramName, filteredValues);
}}
>
<div className={`w-4 h-4 mr-2 border rounded-sm flex items-center justify-center ${
values.length > 0 &&
(paramTestValues[nodeId]?.[paramName] || []).length === values.length
? 'bg-blue-500 border-blue-500'
: (paramTestValues[nodeId]?.[paramName] || []).length > 0
? 'bg-blue-500 border-blue-500 opacity-50'
: 'border-gray-300'
}`}>
{(paramTestValues[nodeId]?.[paramName] || []).length > 0 && (
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="font-medium">
{values.length > 0 &&
(paramTestValues[nodeId]?.[paramName] || []).length === values.length
? 'Deselect All'
: 'Select All'}
</span>
</div>
{/* Scrollable option list */}
<div className="max-h-60 overflow-y-auto">
{values
.filter((value: string) =>
!searchTerms[dropdownKey] ||
value.toLowerCase().includes((searchTerms[dropdownKey] || '').toLowerCase())
)
.map((value: string, i: number) => (
<div
key={i}
className="px-3 py-1.5 hover:bg-gray-100 cursor-pointer text-xs flex items-center text-gray-700"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<div className={`w-4 h-4 mr-2 border rounded-sm flex items-center justify-center ${(paramTestValues[nodeId]?.[paramName] || []).includes(value) ? 'bg-blue-500 border-blue-500' : 'border-gray-300'}`}>
{(paramTestValues[nodeId]?.[paramName] || []).includes(value) && (
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="whitespace-normal overflow-hidden text-ellipsis">{value}</span>
</div>
))}
{/* No search results message */}
{values.filter((value: string) =>
!searchTerms[dropdownKey] ||
value.toLowerCase().includes((searchTerms[dropdownKey] || '').toLowerCase())
).length === 0 && (
<div className="px-3 py-4 text-sm text-gray-500 text-center">
No matching options
</div>
)}
</div>
</div>
)}
</div>
</div>
<div className="mt-2">
<label className="text-xs text-gray-600">Test values</label>
<div className="mt-1 flex flex-wrap gap-1">
{(paramTestValues[nodeId]?.[paramName] || []).map((value: string, idx: number) => (
<div
key={idx}
className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-md flex items-center text-xs"
>
<span>{value}</span>
<svg
className="w-4 h-4 ml-1 cursor-pointer hover:text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
))}
</div>
</div>
</div>
);
} else {
return (
<div key={widgetIndex} className="border-b pb-3">
<div className="flex justify-between items-center">
<label className="text-xs font-medium text-gray-700">{paramName}</label>
</div>
<div className="mt-2">
<label className="text-xs text-gray-600">Test values</label>
<div className="mt-1 flex flex-wrap gap-1">
{(paramTestValues[nodeId]?.[paramName] || []).map((value: string, idx: number) => (
<div
key={idx}
className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-md flex items-center text-xs"
>
<span>{value}</span>
<svg
className="w-4 h-4 ml-1 cursor-pointer hover:text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
))}
<button
className="px-2 py-0.5 border border-dashed border-blue-300 text-blue-600 rounded-md text-xs flex items-center hover:bg-blue-50"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const newValue = `${paramName}${(paramTestValues[nodeId]?.[paramName] || []).length + 1}`;
updateParamTestValues(nodeId, paramName, [...(paramTestValues[nodeId]?.[paramName] || []), newValue]);
}}
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<span>Add</span>
</button>
</div>
</div>
</div>
);
}
})}
</div>
</div>
);
})
) : (
<div className="border rounded-md mb-4 p-4 text-center text-gray-500">
No nodes selected. Please select a node in the workflow to configure parameters.
</div>
)}
<div className="mt-6 flex justify-between">
<button
onClick={(e) => handlePrevious(e)}
className="px-3 py-1.5 text-xs bg-gray-100 text-gray-800 rounded-md hover:bg-gray-200 transition-colors"
>
Previous
</button>
<button
onClick={(e) => handleNext(e)}
className="px-3 py-1.5 text-xs bg-pink-200 text-pink-700 rounded-md hover:bg-pink-300 transition-colors"
>
Next
</button>
</div>
</div>
);
};
@@ -0,0 +1,243 @@
import React, { useState, useEffect } from 'react';
import { app } from '../../../utils/comfyapp';
import { useChatContext } from '../../../context/ChatContext';
interface ConfirmConfigurationScreenProps {
selectedNodes: any[];
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}};
selectedParams: {[key: string]: boolean};
totalCount: number;
errorMessage: string | null;
handlePrevious: (event?: React.MouseEvent) => void;
handleStartGeneration: (event?: React.MouseEvent, selectedNodeId?: number) => void;
handleClose: (event?: React.MouseEvent) => void;
}
// Define node interface for type safety
interface ComfyNode {
id: number;
type: string;
widgets?: any[];
[key: string]: any;
}
export const ConfirmConfigurationScreen: React.FC<ConfirmConfigurationScreenProps> = ({
selectedNodes,
paramTestValues,
selectedParams,
totalCount,
errorMessage,
handlePrevious,
handleStartGeneration,
handleClose
}) => {
// Add state for tracking image nodes and selected node
const [imageNodeIds, setImageNodeIds] = useState<number[]>([]);
const [selectedImageNodeId, setSelectedImageNodeId] = useState<number | null>(null);
const [showNodeSelectionWarning, setShowNodeSelectionWarning] = useState(false);
// Get dispatch from context to track node selection (only using dispatch)
const { dispatch } = useChatContext();
// Function to find all SaveImage and PreviewImage nodes
const findImageNodes = () => {
const nodes = Object.values(app.graph._nodes_by_id) as ComfyNode[];
const saveNodeIds: number[] = [];
const previewNodeIds: number[] = [];
for(const node of nodes) {
if(node.type === "SaveImage") {
saveNodeIds.push(node.id);
} else if(node.type === "PreviewImage") {
previewNodeIds.push(node.id);
}
}
// Combine and set state
const allImageNodeIds = [...saveNodeIds, ...previewNodeIds];
setImageNodeIds(allImageNodeIds);
// If there's exactly one node, set it as selected
if(allImageNodeIds.length === 1) {
setSelectedImageNodeId(allImageNodeIds[0]);
} else if(allImageNodeIds.length > 1) {
// If multiple nodes, enable selection warning
setShowNodeSelectionWarning(true);
setSelectedImageNodeId(null);
}
};
// Effect to find image nodes when component mounts
useEffect(() => {
findImageNodes();
}, []);
// Effect to detect node selection from canvas - using click listener like in Screen 1
useEffect(() => {
// Add a listener for node selection via clicks
const handleNodeSelected = () => {
if (!app.canvas.selected_nodes) return;
const selectedNodesOnCanvas = Object.values(app.canvas.selected_nodes) as ComfyNode[];
if (selectedNodesOnCanvas.length === 0) return;
// Get the first selected node
const selectedNode = selectedNodesOnCanvas[0];
if (!selectedNode) return;
// Check if selected node is a SaveImage or PreviewImage node
if (selectedNode.type === "SaveImage" || selectedNode.type === "PreviewImage") {
console.log("Selected image node:", selectedNode.id);
setSelectedImageNodeId(selectedNode.id);
}
};
// Listen for click events (same as Screen 1)
document.addEventListener('click', handleNodeSelected);
return () => {
document.removeEventListener('click', handleNodeSelected);
};
}, []);
// Modified start generation function
const startGenerationWithSelectedNode = (event?: React.MouseEvent) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
// If we need node selection but don't have it yet, show warning
if (imageNodeIds.length > 1 && !selectedImageNodeId) {
setShowNodeSelectionWarning(true);
return;
}
// Otherwise, proceed with selected node ID
handleStartGeneration(event, selectedImageNodeId || undefined);
};
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<div>
<h3 className="text-base font-medium text-gray-800">Confirm Test Configuration</h3>
<p className="text-xs text-gray-500">
In the selected parameter combinations, there are {totalCount} total runs. Each run will generate a separate image.
</p>
</div>
<button
className="text-gray-400 hover:text-gray-600"
onClick={handleClose}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Display node selection warning */}
{showNodeSelectionWarning && imageNodeIds.length > 1 && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md text-amber-600 text-xs">
<div className="flex items-start">
<div className="flex-shrink-0 mt-0.5">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="ml-2">
{selectedImageNodeId === null
? `Multiple SaveImage and PreviewImage nodes detected with IDs ${imageNodeIds.join(",")}. Please click the node on canvas.`
: `Current node ID is ${selectedImageNodeId}, please click Start Generation again`}
</div>
</div>
</div>
)}
{/* Display error message */}
{errorMessage && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-xs">
<div className="flex items-start">
<div className="flex-shrink-0 mt-0.5">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="ml-2">{errorMessage}</div>
</div>
</div>
)}
{selectedNodes.length > 0 ? (
selectedNodes.map((node, nodeIndex) => {
const nodeType = node.type || "Unknown Node";
const widgets = node.widgets || [];
const nodeId = node.id;
return (
<div key={nodeIndex} className="border rounded-md mb-4 overflow-hidden">
<div className="bg-gray-50 px-3 py-2 text-xs font-medium text-gray-700 border-b">
{nodeType} (ID: {nodeId})
</div>
<div className="p-4 space-y-3">
{widgets.map((widget: any, widgetIndex: number) => {
const paramName = widget.name;
if (!selectedParams[paramName]) {
return null;
}
const testValues = paramTestValues[nodeId]?.[paramName] || [];
const displayValues = Array.isArray(testValues)
? `[${testValues.join(', ')}]`
: JSON.stringify(testValues);
return (
<div key={widgetIndex} className={widgetIndex < widgets.length - 1 ? "flex justify-between items-center border-b pb-2" : "flex justify-between items-center"}>
<label className="font-medium text-gray-700 text-xs">{paramName}</label>
<div className="flex space-x-2 items-center">
<input
type="text"
className="w-64 h-7 border border-gray-300 rounded text-xs px-2 bg-gray-50 cursor-not-allowed text-gray-500"
readOnly
disabled
value={displayValues}
onClick={(e) => e.preventDefault()}
/>
</div>
</div>
);
})}
</div>
</div>
);
})
) : (
<div className="border rounded-md mb-4 p-4 text-center text-gray-500 text-xs">
No nodes selected. Please select a node in the workflow to configure parameters.
</div>
)}
<div className="mt-6 flex justify-between">
<button
onClick={(e) => handlePrevious(e)}
className="px-3 py-1.5 text-xs bg-gray-100 text-gray-800 rounded-md hover:bg-gray-200 transition-colors"
>
Previous
</button>
<button
onClick={(e) => startGenerationWithSelectedNode(e)}
className={`px-3 py-1.5 text-xs rounded-md transition-colors ${
(imageNodeIds.length > 1 && !selectedImageNodeId)
? "bg-gray-200 text-gray-500 cursor-not-allowed"
: "bg-pink-200 text-pink-700 hover:bg-pink-300"
}`}
disabled={imageNodeIds.length > 1 && !selectedImageNodeId}
>
Start Generation
</button>
</div>
</div>
);
};
@@ -0,0 +1,82 @@
import React from 'react';
interface InitialScreenProps {
selectedNodes: any[];
handleParamSelect: (param: string, event?: React.MouseEvent) => void;
selectedParams: {[key: string]: boolean};
handleNext: (event?: React.MouseEvent) => void;
handleClose: (event?: React.MouseEvent) => void;
}
export const InitialScreen: React.FC<InitialScreenProps> = ({
selectedNodes,
handleParamSelect,
selectedParams,
handleNext,
handleClose
}) => {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<div>
<h3 className="text-base font-medium text-gray-800">Select Parameters for Testing</h3>
<p className="text-xs text-gray-500">Choose the parameters you want to include in your batch test</p>
</div>
<button
className="text-gray-400 hover:text-gray-600"
onClick={handleClose}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Dynamically render nodes and their parameters */}
{selectedNodes.map((node, nodeIndex) => {
// Get node title
const nodeTitle = node.title || "Unknown Node";
// Get node widgets/parameters
const nodeWidgets = node.widgets || [];
return (
<div key={`node-${nodeIndex}`} className="border rounded-md mb-4 overflow-hidden">
<div className="bg-gray-50 px-3 py-2 text-sm font-medium text-gray-700 border-b">
{nodeTitle}
</div>
<div className="p-4 space-y-3">
<div className="flex items-center gap-3 flex-wrap">
{nodeWidgets.map((widget: any, widgetIndex: number) => {
const paramName = widget.name;
return (
<div key={`widget-${widgetIndex}`} className="flex items-center">
<div
className={`w-5 h-5 rounded-full border flex items-center justify-center ${
selectedParams[paramName] ? 'border-red-500 bg-red-100' : 'border-gray-300'
}`}
onClick={(e) => handleParamSelect(paramName, e)}
>
{selectedParams[paramName] && <div className="w-3 h-3 rounded-full bg-red-500"></div>}
</div>
<span className="ml-2 text-gray-700 text-xs">{paramName}</span>
</div>
);
})}
</div>
</div>
</div>
);
})}
<div className="mt-6 flex justify-center">
<button
onClick={(e) => handleNext(e)}
className="px-3 py-1.5 text-xs bg-pink-200 text-pink-700 rounded-md hover:bg-pink-300 transition-colors"
>
Next
</button>
</div>
</div>
);
};
@@ -0,0 +1,150 @@
import React from 'react';
import { manageQueue, interruptProcessing } from '../../../apis/comfyApiCustom';
interface ProcessingScreenProps {
selectedNodes: any[];
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}};
selectedParams: {[key: string]: boolean};
totalCount: number;
completedCount: number;
errorMessage: string | null;
handleClose: (event?: React.MouseEvent) => void;
setIsProcessing: React.Dispatch<React.SetStateAction<boolean>>;
setCurrentScreen: React.Dispatch<React.SetStateAction<number>>;
cleanupPolling?: () => void;
}
export const ProcessingScreen: React.FC<ProcessingScreenProps> = ({
selectedNodes,
paramTestValues,
selectedParams,
totalCount,
completedCount,
errorMessage,
handleClose,
setIsProcessing,
setCurrentScreen,
cleanupPolling
}) => {
// 处理取消生成
const handleCancel = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
manageQueue({clear: true});
interruptProcessing();
if (cleanupPolling) {
cleanupPolling();
}
setIsProcessing(false);
setCurrentScreen(2);
};
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 relative">
{/* Semi-transparent overlay */}
<div className="absolute inset-0 bg-gray-500 bg-opacity-30 flex flex-col items-center justify-center z-10">
<div className="text-center text-gray-800 bg-white p-4 rounded-lg shadow-lg">
<div className="mb-2 text-sm font-medium">Processing...</div>
<div className="text-xs mb-3">Completed {completedCount} of {totalCount} runs</div>
<div className="w-48 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-pink-500 transition-all duration-200"
style={{ width: `${(completedCount / totalCount) * 100}%` }}
></div>
</div>
<div className="mt-2 text-xs text-gray-600">
Already generated {completedCount} images
</div>
<button
onClick={handleCancel}
className="mt-4 px-3 py-1.5 text-xs bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition-colors"
>
Cancel
</button>
</div>
</div>
{/* Background content (same as confirm configuration content) */}
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<div>
<h3 className="text-base font-medium text-gray-800">Confirm Test Configuration</h3>
<p className="text-xs text-gray-500">
In the selected parameter combinations, there are {totalCount} total runs. Each run will generate a separate image.
</p>
</div>
<button
className="text-gray-400 hover:text-gray-600"
onClick={handleClose}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Display error message */}
{errorMessage && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-xs">
<div className="flex items-start">
<div className="flex-shrink-0 mt-0.5">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="ml-2">{errorMessage}</div>
</div>
</div>
)}
{selectedNodes.length > 0 ? (
selectedNodes.map((node, nodeIndex) => {
const nodeType = node.type || "Unknown Node";
const widgets = node.widgets || [];
const nodeId = node.id;
return (
<div key={nodeIndex} className="border rounded-md mb-4 overflow-hidden">
<div className="bg-gray-50 px-3 py-2 text-xs font-medium text-gray-700 border-b">
{nodeType} (ID: {nodeId})
</div>
<div className="p-4 space-y-3">
{widgets.map((widget: any, widgetIndex: number) => {
const paramName = widget.name;
if (!selectedParams[paramName]) {
return null;
}
const testValues = paramTestValues[nodeId]?.[paramName] || [];
const displayValues = Array.isArray(testValues)
? `[${testValues.join(', ')}]`
: JSON.stringify(testValues);
return (
<div key={widgetIndex} className={widgetIndex < widgets.length - 1 ? "flex justify-between items-center border-b pb-2" : "flex justify-between items-center"}>
<label className="font-medium text-gray-700">{paramName}</label>
<div className="flex space-x-2 items-center">
<input
type="text"
className="w-64 h-7 border border-gray-300 rounded text-sm px-2 bg-gray-50 cursor-not-allowed text-gray-500"
readOnly
disabled
value={displayValues}
onClick={(e) => e.preventDefault()}
/>
</div>
</div>
);
})}
</div>
</div>
);
})
) : (
<div className="border rounded-md mb-4 p-4 text-center text-gray-500">
No nodes selected. Please select a node in the workflow to configure parameters.
</div>
)}
</div>
);
};
@@ -0,0 +1,212 @@
import React from 'react';
import { app } from '../../../utils/comfyapp';
interface GeneratedImage {
url: string;
params: { [key: string]: any };
}
interface ResultGalleryScreenProps {
generatedImages: GeneratedImage[];
selectedImageIndex: number | null;
handleSelectImage: (index: number, event?: React.MouseEvent) => void;
handleApplySelected: (event?: React.MouseEvent) => void;
handlePrevious: (event?: React.MouseEvent) => void;
handleClose: (event?: React.MouseEvent) => void;
currentPage: number;
handlePageChange: (newPage: number, event?: React.MouseEvent) => void;
imagesPerPage: number;
notificationVisible: boolean;
modalVisible: boolean;
modalImageUrl: string;
openImageModal: (imageUrl: string, event: React.MouseEvent) => void;
closeImageModal: (event?: React.MouseEvent) => void;
}
export const ResultGalleryScreen: React.FC<ResultGalleryScreenProps> = ({
generatedImages,
selectedImageIndex,
handleSelectImage,
handleApplySelected,
handlePrevious,
handleClose,
currentPage,
handlePageChange,
imagesPerPage,
notificationVisible,
modalVisible,
modalImageUrl,
openImageModal,
closeImageModal
}) => {
// Calculate total pages
const actualTotalPages = Math.ceil(generatedImages.length / imagesPerPage);
// Calculate current page's image index range
const startIndex = (currentPage - 1) * imagesPerPage;
const endIndex = Math.min(startIndex + imagesPerPage, generatedImages.length);
const currentImages = generatedImages.slice(startIndex, endIndex);
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<div>
<h3 className="text-base font-medium text-gray-800">Generation Complete</h3>
<p className="text-xs text-gray-500">All {generatedImages.length} images have been generated.</p>
</div>
<button
className="text-gray-400 hover:text-gray-600"
onClick={handleClose}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Notification toast */}
{notificationVisible && (
<div className="fixed top-4 right-4 bg-green-100 border-l-4 border-green-500 text-green-700 p-3 shadow-md rounded animate-fade-in-out z-50">
<div className="flex items-center">
<div className="py-1">
<svg className="h-4 w-4 text-green-500 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<p className="text-xs">Parameters has been applied.</p>
</div>
</div>
)}
{/* Image Modal */}
{modalVisible && (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50" onClick={closeImageModal}>
<div className="bg-white rounded-lg p-2 max-w-4xl max-h-[90vh] relative" onClick={(e) => e.stopPropagation()}>
<button
className="absolute top-2 right-2 text-gray-700 hover:text-gray-900 rounded-full hover:bg-gray-200 p-1"
onClick={closeImageModal}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="overflow-auto max-h-[calc(90vh-4rem)]">
<img
src={modalImageUrl}
alt="Enlarged image"
className="max-w-full h-auto"
/>
</div>
</div>
</div>
)}
<div className="grid grid-cols-3 gap-4 mb-6">
{currentImages.map((image, indexOnPage) => {
const actualIndex = startIndex + indexOnPage;
return (
<div
key={actualIndex}
className={`relative rounded-lg overflow-hidden border ${selectedImageIndex === actualIndex ? 'border-pink-500 ring-2 ring-pink-300' : 'border-gray-200'}`}
onClick={(e) => handleSelectImage(actualIndex, e)}
>
{/* Zoom icon */}
<button
className="absolute top-2 right-2 bg-white bg-opacity-75 rounded-full p-0.5 text-gray-600 hover:text-gray-900 shadow-sm z-10"
onClick={(e) => openImageModal(image.url, e)}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
</svg>
</button>
<div className="aspect-square">
<img
src={image.url}
alt={`Generated image ${actualIndex+1}`}
className="w-full h-full object-cover"
/>
</div>
<div className="p-2 text-xs text-gray-600 bg-white max-h-16 overflow-y-auto">
{Object.entries(image.params)
// Filter out nodeParams and other complex objects from display
.filter(([paramName, value]) =>
paramName !== 'nodeParams' &&
typeof value !== 'object'
)
.map(([paramName, value]) => (
<div key={paramName}>{paramName}: {String(value)}</div>
))
}
</div>
</div>
);
})}
</div>
{/* Pagination controls */}
{actualTotalPages > 1 && (
<div className="flex justify-center items-center space-x-2 mt-4 mb-4">
<button
onClick={(e) => handlePageChange(1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded ${currentPage === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M15.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
<path fillRule="evenodd" d="M8.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L4.414 10l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>
</button>
<button
onClick={(e) => handlePageChange(currentPage - 1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded ${currentPage === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
<div className="px-2 py-1 text-xs text-gray-800">
Page {currentPage} of {actualTotalPages}
</div>
<button
onClick={(e) => handlePageChange(currentPage + 1, e)}
disabled={currentPage === actualTotalPages}
className={`px-2 py-1 rounded ${currentPage === actualTotalPages ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
<button
onClick={(e) => handlePageChange(actualTotalPages, e)}
disabled={currentPage === actualTotalPages}
className={`px-2 py-1 rounded ${currentPage === actualTotalPages ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 15.707a1 1 0 001.414 0l5-5a1 1 0 000-1.414l-5-5a1 1 0 00-1.414 1.414L8.586 10 4.293 14.293a1 1 0 000 1.414z" clipRule="evenodd" />
<path fillRule="evenodd" d="M11.293 15.707a1 1 0 001.414 0l5-5a1 1 0 000-1.414l-5-5a1 1 0 00-1.414 1.414L15.586 10l-4.293 4.293a1 1 0 000 1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
)}
<div className="mt-6 flex justify-between">
<button
onClick={(e) => handlePrevious(e)}
className="px-3 py-1.5 text-xs bg-gray-100 text-gray-800 rounded-md hover:bg-gray-200 transition-colors"
>
Previous
</button>
<button
onClick={(e) => handleApplySelected(e)}
className="px-3 py-1.5 text-xs bg-pink-200 text-pink-700 rounded-md hover:bg-pink-300 transition-colors"
disabled={selectedImageIndex === null}
>
Apply Selected
</button>
</div>
</div>
);
};
@@ -0,0 +1,194 @@
// 生成数值类型的测试参数
export const generateNumericTestValues = (
min: number,
max: number,
step: number,
precision: number = 0
): number[] => {
// Ensure all parameters have valid values
min = isNaN(min) ? 0 : min;
max = isNaN(max) ? 100 : max;
step = isNaN(step) || step <= 0 ? 1 : step;
// Ensure max >= min
if (max < min) {
max = min;
}
// Calculate count based on the actual step
const count = Math.floor((max - min) / step) + 1;
if (count <= 1) return [min];
// Generate values based on the actual step
const values = [];
let current = min;
if (count <= 10){
while (current <= max && values.length <= 10) {
// Apply precision based on the parameter
const factor = Math.pow(10, precision);
const roundedValue = Math.round(current * factor) / factor;
values.push(roundedValue);
current += step;
}
}
else{
// For large ranges, generate evenly distributed values
const range = max - min;
const step = range / 9; // 9 steps to get 10 values
for (let i = 0; i <= 9; i++) {
// Calculate the value at this position
const value = min + (step * i);
// Apply precision
const factor = Math.pow(10, precision);
const roundedValue = Math.round(value * factor) / factor;
values.push(roundedValue);
}
}
return values;
};
// WidgetParamConf接口定义
export interface WidgetParamConf {
nodeId: number;
paramName: string;
paramValue: string;
}
// 生成所有参数组合
export const generateParameterCombinations = (
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}}
): WidgetParamConf[][] => {
// First, we need to get all the different parameter combinations
const result: WidgetParamConf[][] = [];
// Create a list of all parameter combinations for each node
const nodeParamCombinations: {[nodeId: string]: Array<WidgetParamConf[]>} = {};
// For each node, generate all combinations of its parameters
Object.keys(paramTestValues).forEach(nodeIdStr => {
const nodeId = parseInt(nodeIdStr);
const nodeParams = paramTestValues[nodeIdStr] || {};
// Get all parameter names that have values
const paramNames = Object.keys(nodeParams).filter(
paramName => nodeParams[paramName] && nodeParams[paramName].length > 0
);
if (paramNames.length === 0) return;
// Create all possible combinations of parameter values for this node
const combinations: WidgetParamConf[][] = [[]];
// For each parameter, create combinations with all its values
paramNames.forEach(paramName => {
const values = nodeParams[paramName];
const newCombinations: WidgetParamConf[][] = [];
// For each existing combination
combinations.forEach(combo => {
// For each value of the current parameter
values.forEach(value => {
// Create a new combination by adding this parameter value
const newCombo = [...combo, {
nodeId,
paramName,
paramValue: String(value)
}];
newCombinations.push(newCombo);
});
});
// Replace combinations with the new ones
combinations.length = 0;
combinations.push(...newCombinations);
});
// Store all combinations for this node
nodeParamCombinations[nodeIdStr] = combinations;
});
// If no nodes have parameters, return empty result
if (Object.keys(nodeParamCombinations).length === 0) {
return result;
}
// If there's only one node, return its combinations directly
if (Object.keys(nodeParamCombinations).length === 1) {
const nodeId = Object.keys(nodeParamCombinations)[0];
return nodeParamCombinations[nodeId];
}
// For multiple nodes, we need to create combinations across nodes
const nodeIds = Object.keys(nodeParamCombinations);
// Start with combinations from the first node
let allCombinations = nodeParamCombinations[nodeIds[0]];
// For each additional node, create combinations with all previous nodes
for (let i = 1; i < nodeIds.length; i++) {
const nodeCombinations = nodeParamCombinations[nodeIds[i]];
const newCombinations: WidgetParamConf[][] = [];
// For each existing combination across previous nodes
allCombinations.forEach(existingCombo => {
// For each combination from the current node
nodeCombinations.forEach(nodeCombo => {
// Combine them
newCombinations.push([...existingCombo, ...nodeCombo]);
});
});
// Update all combinations
allCombinations = newCombinations;
}
return allCombinations;
};
// Generate dynamic parameters for a specific combination index
export const generateDynamicParams = (
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}},
index: number
) => {
// Create object to store dynamic parameters
const dynamicParams: {
[key: string]: any,
nodeParams?: {[nodeId: string]: {[paramName: string]: any}}
} = {};
// Initialize nodeParams structure
dynamicParams.nodeParams = {};
// Get all parameter combinations
const allCombinations = generateParameterCombinations(paramTestValues);
// Use the index to select a specific combination
if (index < allCombinations.length) {
const selectedCombination = allCombinations[index];
// Process each parameter in the combination
selectedCombination.forEach(param => {
const nodeId = param.nodeId.toString();
const paramName = param.paramName;
const paramValue = param.paramValue;
// Initialize node if needed
if (!dynamicParams.nodeParams![nodeId]) {
dynamicParams.nodeParams![nodeId] = {};
}
// Add to nodeParams structure
dynamicParams.nodeParams![nodeId][paramName] = paramValue;
// Also add to flat structure for backwards compatibility and display
dynamicParams[paramName] = paramValue;
});
}
return dynamicParams;
};
+20
View File
@@ -9,6 +9,16 @@
import React, { createContext, useContext, useReducer, Dispatch } from 'react';
import { Message } from '../types/types';
// Add tab type definition
export type TabType = 'chat' | 'parameter-debug';
// Interface for tracking ParameterDebugInterface screen state
export interface ScreenState {
currentScreen: number;
isProcessing: boolean;
isCompleted: boolean;
}
interface ChatState {
messages: Message[];
selectedNode: any | null;
@@ -16,6 +26,8 @@ interface ChatState {
loading: boolean;
sessionId: string | null;
showChat: boolean;
activeTab: TabType;
screenState: ScreenState | null; // Add screen state
}
type ChatAction =
@@ -27,6 +39,8 @@ type ChatAction =
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_SESSION_ID'; payload: string }
| { type: 'SET_SHOW_CHAT'; payload: boolean }
| { type: 'SET_ACTIVE_TAB'; payload: TabType }
| { type: 'SET_SCREEN_STATE'; payload: ScreenState | null } // Add action for setting screen state
| { type: 'CLEAR_MESSAGES' };
const initialState: ChatState = {
@@ -36,6 +50,8 @@ const initialState: ChatState = {
loading: false,
sessionId: null,
showChat: false,
activeTab: 'chat',
screenState: null, // Initialize as null
};
function chatReducer(state: ChatState, action: ChatAction): ChatState {
@@ -61,6 +77,10 @@ function chatReducer(state: ChatState, action: ChatAction): ChatState {
return { ...state, sessionId: action.payload };
case 'SET_SHOW_CHAT':
return { ...state, showChat: action.payload };
case 'SET_ACTIVE_TAB':
return { ...state, activeTab: action.payload };
case 'SET_SCREEN_STATE':
return { ...state, screenState: action.payload };
case 'CLEAR_MESSAGES':
return { ...state, messages: [] };
default:
+17 -9
View File
@@ -3,18 +3,26 @@ import { app } from '../utils/comfyapp';
import { useChatContext } from '../context/ChatContext';
export function useNodeSelection(enabled: boolean = true) {
const { dispatch } = useChatContext();
const { state, dispatch } = useChatContext();
const { activeTab, screenState } = state;
useEffect(() => {
if (!enabled) return;
const handleNodeSelection = () => {
const selectedNodes = app.canvas.selected_nodes;
if (Object.keys(selectedNodes ?? {}).length) {
const nodeInfo = Object.values(selectedNodes)[0];
dispatch({ type: 'SET_SELECTED_NODE', payload: nodeInfo });
} else {
dispatch({ type: 'SET_SELECTED_NODE', payload: null });
const handleNodeSelection = (event: MouseEvent) => {
// and only when in parameter-debug tab
const isParameterDebugTab = activeTab === 'parameter-debug';
const isChatTab = activeTab === 'chat';
const isAllowedScreenParameterDebug = !screenState || screenState.currentScreen === 0;
if ((isChatTab) || (isAllowedScreenParameterDebug && isParameterDebugTab)) {
const selectedNodes = app.canvas.selected_nodes;
if (Object.keys(selectedNodes ?? {}).length) {
const nodeInfo = Object.values(selectedNodes);
dispatch({ type: 'SET_SELECTED_NODE', payload: nodeInfo });
} else {
dispatch({ type: 'SET_SELECTED_NODE', payload: null });
}
}
};
@@ -22,5 +30,5 @@ export function useNodeSelection(enabled: boolean = true) {
return () => {
document.removeEventListener("click", handleNodeSelection);
};
}, [dispatch, enabled]);
}, [dispatch, enabled, activeTab, screenState]);
}
+44
View File
@@ -0,0 +1,44 @@
import { useEffect } from 'react';
import { app } from '../utils/comfyapp';
import { useChatContext } from '../context/ChatContext';
export function useNodeSelection(enabled: boolean = true) {
const { dispatch } = useChatContext();
useEffect(() => {
if (!enabled) return;
const handleNodeSelected = () => {
if (!app.canvas.selected_nodes || Object.keys(app.canvas.selected_nodes).length === 0) {
dispatch({ type: 'SET_SELECTED_NODE', payload: null });
return;
}
// Get the first selected node
// const selectedNodeId = Object.keys(app.canvas.selected_nodes)[0];
// const selectedNode = app.canvas.selected_nodes[selectedNodeId];
// if (!selectedNode) return;
// Extract node information
// const nodeInfo = {
// id: selectedNodeId,
// title: selectedNode.title,
// type: selectedNode.type,
// comfyClass: selectedNode.comfyClass || selectedNode.type,
// widgets: selectedNode.widgets,
// properties: selectedNode.properties,
// inputs: selectedNode.inputs,
// outputs: selectedNode.outputs
// };
dispatch({ type: 'SET_SELECTED_NODE', payload: Object.values(app.canvas.selected_nodes) });
};
// Listen for selection events
document.addEventListener('comfy:node_selected', handleNodeSelected);
return () => {
document.removeEventListener('comfy:node_selected', handleNodeSelected);
};
}, [dispatch, enabled]);
}
+291 -6
View File
@@ -588,6 +588,20 @@
--tw-prose-invert-th-borders: #525252;
--tw-prose-invert-td-borders: #404040;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.\!visible {
visibility: visible !important;
}
.visible {
visibility: visible;
}
@@ -670,6 +684,9 @@
.right-3 {
right: 0.75rem;
}
.right-4 {
right: 1rem;
}
.top-0 {
top: 0px;
}
@@ -679,6 +696,9 @@
.top-2 {
top: 0.5rem;
}
.top-4 {
top: 1rem;
}
.z-10 {
z-index: 10;
}
@@ -706,9 +726,6 @@
.-ml-1 {
margin-left: -0.25rem;
}
.-ml-2 {
margin-left: -0.5rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
@@ -724,6 +741,12 @@
.mb-6 {
margin-bottom: 1.5rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.ml-3 {
margin-left: 0.75rem;
}
@@ -733,6 +756,12 @@
.mr-2 {
margin-right: 0.5rem;
}
.mr-3 {
margin-right: 0.75rem;
}
.mt-0\.5 {
margin-top: 0.125rem;
}
.mt-1 {
margin-top: 0.25rem;
}
@@ -745,6 +774,9 @@
.mt-4 {
margin-top: 1rem;
}
.mt-6 {
margin-top: 1.5rem;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
@@ -775,12 +807,15 @@
.hidden {
display: none;
}
.aspect-square {
aspect-ratio: 1 / 1;
}
.h-1 {
height: 0.25rem;
}
.h-10 {
height: 2.5rem;
}
.h-12 {
height: 3rem;
}
.h-14 {
height: 3.5rem;
}
@@ -802,6 +837,12 @@
.h-5 {
height: 1.25rem;
}
.h-6 {
height: 1.5rem;
}
.h-7 {
height: 1.75rem;
}
.h-8 {
height: 2rem;
}
@@ -814,9 +855,24 @@
.h-full {
height: 100%;
}
.max-h-16 {
max-height: 4rem;
}
.max-h-60 {
max-height: 15rem;
}
.max-h-\[400px\] {
max-height: 400px;
}
.max-h-\[40vh\] {
max-height: 40vh;
}
.max-h-\[90vh\] {
max-height: 90vh;
}
.max-h-\[calc\(90vh-4rem\)\] {
max-height: calc(90vh - 4rem);
}
.min-h-\[80px\] {
min-height: 80px;
}
@@ -826,12 +882,18 @@
.w-1\/2 {
width: 50%;
}
.w-10 {
width: 2.5rem;
}
.w-12 {
width: 3rem;
}
.w-14 {
width: 3.5rem;
}
.w-16 {
width: 4rem;
}
.w-2 {
width: 0.5rem;
}
@@ -847,6 +909,9 @@
.w-40 {
width: 10rem;
}
.w-48 {
width: 12rem;
}
.w-5 {
width: 1.25rem;
}
@@ -877,6 +942,18 @@
.min-w-\[400px\] {
min-width: 400px;
}
.max-w-2xl {
max-width: 42rem;
}
.max-w-4xl {
max-width: 56rem;
}
.max-w-full {
max-width: 100%;
}
.max-w-lg {
max-width: 32rem;
}
.flex-1 {
flex: 1 1 0%;
}
@@ -895,6 +972,9 @@
--tw-translate-y: -100%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.transform {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
@keyframes bounce {
0%, 100% {
transform: translateY(-25%);
@@ -940,9 +1020,17 @@
.resize-none {
resize: none;
}
.resize-y {
resize: vertical;
}
.resize {
resize: both;
}
.appearance-none {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -992,6 +1080,16 @@
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
@@ -1002,6 +1100,9 @@
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
.overflow-auto {
overflow: auto;
}
.overflow-hidden {
overflow: hidden;
}
@@ -1017,6 +1118,9 @@
.scroll-smooth {
scroll-behavior: smooth;
}
.text-ellipsis {
text-overflow: ellipsis;
}
.whitespace-normal {
white-space: normal;
}
@@ -1041,6 +1145,9 @@
.rounded-md {
border-radius: 0.375rem;
}
.rounded-sm {
border-radius: 0.125rem;
}
.rounded-xl {
border-radius: 0.75rem;
}
@@ -1059,6 +1166,12 @@
.border-b {
border-bottom-width: 1px;
}
.border-b-2 {
border-bottom-width: 2px;
}
.border-l-4 {
border-left-width: 4px;
}
.border-t {
border-top-width: 1px;
}
@@ -1079,10 +1192,26 @@
--tw-border-opacity: 1;
border-color: rgb(253 230 138 / var(--tw-border-opacity, 1));
}
.border-blue-100 {
--tw-border-opacity: 1;
border-color: rgb(219 234 254 / var(--tw-border-opacity, 1));
}
.border-blue-200 {
--tw-border-opacity: 1;
border-color: rgb(191 219 254 / var(--tw-border-opacity, 1));
}
.border-blue-300 {
--tw-border-opacity: 1;
border-color: rgb(147 197 253 / var(--tw-border-opacity, 1));
}
.border-blue-500 {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
}
.border-blue-600 {
--tw-border-opacity: 1;
border-color: rgb(37 99 235 / var(--tw-border-opacity, 1));
}
.border-gray-100 {
--tw-border-opacity: 1;
border-color: rgb(243 244 246 / var(--tw-border-opacity, 1));
@@ -1107,9 +1236,29 @@
--tw-border-opacity: 1;
border-color: rgb(187 247 208 / var(--tw-border-opacity, 1));
}
.border-green-500 {
--tw-border-opacity: 1;
border-color: rgb(34 197 94 / var(--tw-border-opacity, 1));
}
.border-pink-500 {
--tw-border-opacity: 1;
border-color: rgb(236 72 153 / var(--tw-border-opacity, 1));
}
.border-red-200 {
--tw-border-opacity: 1;
border-color: rgb(254 202 202 / var(--tw-border-opacity, 1));
}
.border-red-500 {
--tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity, 1));
}
.border-transparent {
border-color: transparent;
}
.border-yellow-200 {
--tw-border-opacity: 1;
border-color: rgb(254 240 138 / var(--tw-border-opacity, 1));
}
.border-t-blue-500 {
--tw-border-opacity: 1;
border-top-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
@@ -1165,6 +1314,10 @@
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
}
.bg-gray-500 {
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity, 1));
}
.bg-gray-700 {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
@@ -1185,14 +1338,34 @@
--tw-bg-opacity: 1;
background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
}
.bg-indigo-100 {
--tw-bg-opacity: 1;
background-color: rgb(224 231 255 / var(--tw-bg-opacity, 1));
}
.bg-pink-200 {
--tw-bg-opacity: 1;
background-color: rgb(251 207 232 / var(--tw-bg-opacity, 1));
}
.bg-pink-500 {
--tw-bg-opacity: 1;
background-color: rgb(236 72 153 / var(--tw-bg-opacity, 1));
}
.bg-purple-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 232 255 / var(--tw-bg-opacity, 1));
}
.bg-red-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
}
.bg-red-50 {
--tw-bg-opacity: 1;
background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1));
}
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1));
}
.bg-transparent {
background-color: transparent;
}
@@ -1200,9 +1373,19 @@
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
}
.bg-yellow-50 {
--tw-bg-opacity: 1;
background-color: rgb(254 252 232 / var(--tw-bg-opacity, 1));
}
.bg-opacity-30 {
--tw-bg-opacity: 0.3;
}
.bg-opacity-50 {
--tw-bg-opacity: 0.5;
}
.bg-opacity-75 {
--tw-bg-opacity: 0.75;
}
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
@@ -1214,6 +1397,9 @@
.to-amber-100 {
--tw-gradient-to: #fef3c7 var(--tw-gradient-to-position);
}
.fill-current {
fill: currentColor;
}
.object-cover {
-o-object-fit: cover;
object-fit: cover;
@@ -1297,6 +1483,12 @@
.pb-10 {
padding-bottom: 2.5rem;
}
.pb-2 {
padding-bottom: 0.5rem;
}
.pb-3 {
padding-bottom: 0.75rem;
}
.pr-12 {
padding-right: 3rem;
}
@@ -1359,6 +1551,9 @@
font-size: 0.75rem;
line-height: 1rem;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
@@ -1407,6 +1602,10 @@
--tw-text-opacity: 1;
color: rgb(30 58 138 / var(--tw-text-opacity, 1));
}
.text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
}
.text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
@@ -1431,6 +1630,10 @@
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
}
.text-green-500 {
--tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity, 1));
}
.text-green-700 {
--tw-text-opacity: 1;
color: rgb(21 128 61 / var(--tw-text-opacity, 1));
@@ -1443,10 +1646,26 @@
--tw-text-opacity: 1;
color: rgb(20 83 45 / var(--tw-text-opacity, 1));
}
.text-indigo-600 {
--tw-text-opacity: 1;
color: rgb(79 70 229 / var(--tw-text-opacity, 1));
}
.text-pink-700 {
--tw-text-opacity: 1;
color: rgb(190 24 93 / var(--tw-text-opacity, 1));
}
.text-purple-700 {
--tw-text-opacity: 1;
color: rgb(126 34 206 / var(--tw-text-opacity, 1));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
.text-red-600 {
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity, 1));
}
.text-red-700 {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
@@ -1475,6 +1694,9 @@
.opacity-40 {
opacity: 0.4;
}
.opacity-50 {
opacity: 0.5;
}
.opacity-75 {
opacity: 0.75;
}
@@ -1493,6 +1715,11 @@
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
@@ -1502,6 +1729,15 @@
outline: 2px solid transparent;
outline-offset: 2px;
}
.ring-2 {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.ring-pink-300 {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(249 168 212 / var(--tw-ring-opacity, 1));
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
@@ -1567,6 +1803,15 @@
}
}
.hover\:border-b-2:hover {
border-bottom-width: 2px;
}
.hover\:border-blue-300:hover {
--tw-border-opacity: 1;
border-color: rgb(147 197 253 / var(--tw-border-opacity, 1));
}
.hover\:border-blue-500:hover {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
@@ -1581,11 +1826,21 @@
background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
}
.hover\:bg-blue-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(191 219 254 / var(--tw-bg-opacity, 1));
}
.hover\:bg-blue-300:hover {
--tw-bg-opacity: 1;
background-color: rgb(147 197 253 / var(--tw-bg-opacity, 1));
}
.hover\:bg-blue-50:hover {
--tw-bg-opacity: 1;
background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1));
}
.hover\:bg-blue-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
@@ -1601,6 +1856,11 @@
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-300:hover {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
@@ -1616,6 +1876,11 @@
background-color: rgb(187 247 208 / var(--tw-bg-opacity, 1));
}
.hover\:bg-pink-300:hover {
--tw-bg-opacity: 1;
background-color: rgb(249 168 212 / var(--tw-bg-opacity, 1));
}
.hover\:bg-purple-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(233 213 255 / var(--tw-bg-opacity, 1));
@@ -1635,6 +1900,16 @@
color: rgb(146 64 14 / var(--tw-text-opacity, 1));
}
.hover\:text-blue-500:hover {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity, 1));
}
.hover\:text-blue-600:hover {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
}
.hover\:text-blue-700:hover {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity, 1));
@@ -1650,6 +1925,16 @@
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
}
.hover\:text-gray-900:hover {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
}
.hover\:text-red-700:hover {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
}
.hover\:opacity-100:hover {
opacity: 1;
}
+236
View File
@@ -0,0 +1,236 @@
import { getHistory, runPrompt } from "../apis/comfyApiCustom";
import { app } from "../utils/comfyapp";
export type WidgetParamConf = {
nodeId: number;
paramName: string;
paramValue: string;
}
function updatePrompt(prompt_output: any, paramConfigs: WidgetParamConf[]): any {
try {
if (!prompt_output) {
console.error("Invalid prompt_output: ", prompt_output);
return prompt_output;
}
// Apply each parameter configuration
for (const config of paramConfigs) {
if (!prompt_output[config.nodeId]) {
console.error(`Node with ID ${config.nodeId} not found`);
continue;
}
if (!prompt_output[config.nodeId]["inputs"]) {
console.error(`Inputs not found for node with ID ${config.nodeId}`);
continue;
}
// Update each parameter for this node
prompt_output[config.nodeId]["inputs"][config.paramName] = config.paramValue;
}
return prompt_output;
} catch (error) {
console.error("Error updating prompt:", error);
return prompt_output;
}
}
export async function queuePrompt(paramConfigs: WidgetParamConf[]): Promise<any> {
const prompt = await app.graphToPrompt()
const updated_prompt = updatePrompt(prompt.output, paramConfigs)
console.log("queuePrompt updated_prompt:", updated_prompt);
const request_body = {
prompt: updated_prompt,
client_id: app.api.clientId,
extra_data: {
extra_pageinfo: {
workflow: prompt.workflow,
}
}
}
console.debug("queuePrompt request_body.prompt:", updated_prompt);
const response = await runPrompt(request_body);
console.debug("queuePrompt response:", response);
return response;
}
function createErrorImage(errorMessage: string): string {
// Create a data URL for an error image with the provided error message
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas dimensions
canvas.width = 512;
canvas.height = 512;
if (ctx) {
// Fill background
ctx.fillStyle = '#f8d7da';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw border
ctx.strokeStyle = '#dc3545';
ctx.lineWidth = 4;
ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20);
// Draw error icon
ctx.fillStyle = '#dc3545';
ctx.beginPath();
ctx.arc(canvas.width / 2, 150, 50, 0, 2 * Math.PI);
ctx.fill();
// Draw exclamation mark
ctx.fillStyle = 'white';
ctx.font = 'bold 80px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('!', canvas.width / 2, 150);
// Draw error text
ctx.fillStyle = '#721c24';
ctx.font = '20px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Handle multi-line text
const maxWidth = canvas.width - 60;
const lineHeight = 30;
const words = errorMessage.split(' ');
let line = '';
let y = 250;
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i] + ' ';
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && i > 0) {
ctx.fillText(line, canvas.width / 2, y);
line = words[i] + ' ';
y += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, canvas.width / 2, y);
}
return canvas.toDataURL('image/png');
}
export async function getOutputImagesByPromptId(promptId: string): Promise<{[nodeId: string]: string[]}> {
try {
if(!promptId || promptId === "") {
console.log("No prompt ID provided");
return { "1": [createErrorImage("Fail to generate prompt ID")] };
}
// Get the history for the prompt
const history = await getHistory(promptId);
if (!history || !history[promptId]) {
console.log("Not finished for prompt ID:", promptId);
return {};
}
const promptHistory = history[promptId];
if(promptHistory.status && promptHistory.status.status_str === "error") {
console.error("Error for prompt ID:", promptId);
const messages = promptHistory.status.messages;
if(messages && messages.length > 0) {
const lastMessage = messages[messages.length - 1];
if(lastMessage && lastMessage.length > 0) {
const errorMessage = lastMessage[0];
// Return an error image with the error message
const errorImage = createErrorImage(errorMessage);
return { "1": [errorImage] }; // Return with default nodeId of 1
}
}
}
const outputImages: {[nodeId: string]: string[]} = {};
// Process all outputs in the history
if (promptHistory.outputs) {
for (const nodeId in promptHistory.outputs) {
const nodeOutput = promptHistory.outputs[nodeId];
if (nodeOutput && nodeOutput.images) {
outputImages[nodeId] = [];
// Process each image for this node
for (const img of nodeOutput.images) {
if (img && img.filename && img.type) {
// Construct the image URL using the ComfyUI view endpoint
// const imageUrl = `${window.location.origin}/view?filename=${encodeURIComponent(img.filename)}&type=${encodeURIComponent(img.type)}${img.subfolder ? '&subfolder=' + encodeURIComponent(img.subfolder) : ''}`;
//app.graph._nodes_by_id[3].imgs[0].currentSrc
const urlBase = app.graph._nodes_by_id[nodeId].imgs[0].currentSrc.split("?")[0];
outputImages[nodeId].push(urlBase + "?filename=" + encodeURIComponent(img.filename) + "&type=" + encodeURIComponent(img.type) + (img.subfolder ? "&subfolder=" + encodeURIComponent(img.subfolder) : ""));
}
}
console.log("outputImages for nodeId:", nodeId, outputImages[nodeId]);
}
}
}
return outputImages;
} catch (error) {
console.error("Error getting output images from prompt:", error);
throw error;
}
}
// Define node interface for type safety
interface ComfyNode {
id: number;
type: string;
[key: string]: any;
}
export function getOnlyOneImageNode() {
const nodes = Object.values(app.graph._nodes_by_id) as ComfyNode[];
const saveNodeIds = [];
const previewNodeIds = [];
for(const node of nodes) {
if(node.type === "SaveImage") {
saveNodeIds.push(node.id);
} else if(node.type === "PreviewImage") {
previewNodeIds.push(node.id);
}
}
console.log("saveNodeIds:", saveNodeIds);
console.log("previewNodeIds:", previewNodeIds);
if(saveNodeIds.length === 0 && previewNodeIds.length === 0) {
throw new Error("No SaveImage or PreviewImage node found, please add one to the graph");
}
if(saveNodeIds.length === 1) {
return saveNodeIds[0];
} else if(saveNodeIds.length == 0 && previewNodeIds.length === 1) {
return previewNodeIds[0];
} else {
// Return all node IDs as an array so the UI can handle selection
return null;
}
}
export async function getOutputImageByPromptId(promptId: string, nodeId: Number): Promise<string | null> {
const outputImages = await getOutputImagesByPromptId(promptId);
// If no images were found, return an empty array
if (Object.keys(outputImages).length === 0) {
return null;
}
// Find the maximum nodeId
// const nodeIds = Object.keys(outputImages).map(id => parseInt(id));
// const maxNodeId = Math.max(...nodeIds).toString();
if (outputImages[nodeId.toString()] && outputImages[nodeId.toString()].length > 0) {
return outputImages[nodeId.toString()][0];
}
return null;
}
+128 -21
View File
@@ -1,3 +1,11 @@
/*
* @Author: ai-business-hql ai.bussiness.hql@gmail.com
* @Date: 2025-03-20 15:15:20
* @LastEditors: ai-business-hql ai.bussiness.hql@gmail.com
* @LastEditTime: 2025-03-26 15:48:39
* @FilePath: /comfyui_copilot/ui/src/workflowChat/workflowChat.tsx
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
@@ -22,6 +30,9 @@ import { MemoizedReactMarkdown } from "../components/markdown";
import remarkGfm from 'remark-gfm';
import rehypeExternalLinks from 'rehype-external-links';
// Define the Tab type - We should import this from context to ensure consistency
import type { TabType } from '../context/ChatContext';
interface WorkflowChatProps {
onClose?: () => void;
visible?: boolean;
@@ -87,9 +98,62 @@ const Announcement = ({ message, onClose }: { message: string, onClose: () => vo
);
};
// Parameter Debug Tab Component
const ParameterDebugTab = () => {
const { state, dispatch } = useChatContext();
const { selectedNode, screenState } = state;
const selectedNodes = selectedNode ? selectedNode : [];
const ParameterDebugInterface = React.lazy(() =>
import("../components/debug/ParameterDebugInterfaceNew").then(module => ({
default: module.ParameterDebugInterface
}))
);
const handleCloseParameterDebug = () => {
// Clear selected nodes and screen state
dispatch({ type: 'SET_SELECTED_NODE', payload: null });
dispatch({ type: 'SET_SCREEN_STATE', payload: null });
};
return (
<div className="flex-1 flex flex-col overflow-y-auto">
<React.Suspense fallback={<div>Loading...</div>}>
<ParameterDebugInterface
selectedNodes={selectedNodes}
visible={true}
onClose={handleCloseParameterDebug}
/>
</React.Suspense>
</div>
);
};
// Tab component
const TabButton = ({
active,
onClick,
children
}: {
active: boolean;
onClick: () => void;
children: React.ReactNode
}) => (
<button
onClick={onClick}
className={`px-4 py-2 font-medium text-sm transition-colors duration-200 ${
active
? "text-blue-600 border-b-2 border-blue-600"
: "text-gray-600 hover:text-blue-500 hover:border-b-2 hover:border-blue-300"
}`}
>
{children}
</button>
);
export default function WorkflowChat({ onClose, visible = true, triggerUsage = false, onUsageTriggered }: WorkflowChatProps) {
const { state, dispatch } = useChatContext();
const { messages, installedNodes, loading, sessionId, selectedNode } = state;
const { messages, installedNodes, loading, sessionId, selectedNode, activeTab } = state;
const messageDivRef = useRef<HTMLDivElement>(null);
const [input, setInput] = useState<string>('');
const [latestInput, setLatestInput] = useState<string>('');
@@ -103,9 +167,11 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
const [announcement, setAnnouncement] = useState<string>('');
const [showAnnouncement, setShowAnnouncement] = useState<boolean>(false);
// 使用自定义 hooks,只在visible为true时启用
useMousePosition(visible);
// 使用自定义 hooks,只在visible为true且activeTab为chat时启用
useMousePosition(visible && activeTab === 'chat');
useNodeSelection(visible);
// 使用自定义 hooks
const {
isResizing: resizableIsResizing,
setIsResizing: resizableSetIsResizing,
@@ -125,13 +191,15 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
}, [messages])
useEffect(() => {
if (activeTab !== 'chat') return;
const fetchInstalledNodes = async () => {
const nodes = await getInstalledNodes();
console.log('[WorkflowChat] Received installed nodes:', nodes.length);
dispatch({ type: 'SET_INSTALLED_NODES', payload: nodes });
};
fetchInstalledNodes();
}, []);
}, [activeTab]);
// 获取历史消息
const fetchMessages = async (sid: string) => {
@@ -145,6 +213,8 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
};
useEffect(() => {
if (activeTab !== 'chat') return;
let sid = localStorage.getItem("sessionId");
if (sid) {
dispatch({ type: 'SET_SESSION_ID', payload: sid });
@@ -154,7 +224,7 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
dispatch({ type: 'SET_SESSION_ID', payload: sid });
localStorage.setItem("sessionId", sid);
}
}, []);
}, [activeTab]);
// 使用防抖处理宽度调整
const handleMouseMoveForResize = React.useCallback((e: MouseEvent) => {
@@ -460,14 +530,16 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
}, [uploadedImages]);
useEffect(() => {
if (triggerUsage && onUsageTriggered) {
if (triggerUsage && onUsageTriggered && activeTab === 'chat') {
handleSendMessageWithIntent('node_explain');
onUsageTriggered();
}
}, [triggerUsage]);
}, [triggerUsage, activeTab]);
// 获取公告内容
useEffect(() => {
if (!visible || activeTab !== 'chat') return;
const fetchAnnouncement = async () => {
try {
// 检查今天是否已经显示过公告
@@ -489,16 +561,24 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
}
};
if (visible) {
fetchAnnouncement();
}
}, [visible]);
fetchAnnouncement();
}, [visible, activeTab]);
// 关闭公告
const handleCloseAnnouncement = () => {
setShowAnnouncement(false);
};
// Handle tab change
const handleTabChange = (tab: TabType) => {
dispatch({ type: 'SET_ACTIVE_TAB', payload: tab });
};
// Initialize the parameter debug tab component with lazy loading
const parameterDebugTabComponent = React.useMemo(() => (
<ParameterDebugTab />
), []);
return (
<div
className="fixed right-0 shadow-lg bg-white duration-200 ease-out"
@@ -523,23 +603,39 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
onClear={handleClearMessages}
hasMessages={messages.length > 0}
onHeightResize={handleHeightResize}
title={`ComfyUI-Copilot`}
/>
{/* 将公告移到 ChatHeader 下方 */}
{showAnnouncement && announcement && (
{/* Tab navigation */}
<div className="flex border-b border-gray-200 mt-2">
<TabButton
active={activeTab === 'chat'}
onClick={() => handleTabChange('chat')}
>
Chat
</TabButton>
<TabButton
active={activeTab === 'parameter-debug'}
onClick={() => handleTabChange('parameter-debug')}
>
GenLab
</TabButton>
</div>
{/* 将公告移到 ChatHeader 下方和Tab导航下方 */}
{showAnnouncement && announcement && activeTab === 'chat' && (
<Announcement
message={announcement}
onClose={handleCloseAnnouncement}
/>
)}
<div>
{installedNodes.map((node: any) => (
<div key={node.name}>{node.name}</div>
))}
</div>
<div className="flex-1 overflow-y-auto p-4 scroll-smooth" ref={messageDivRef}>
{/* Tab content - Both tabs are mounted but only the active one is displayed */}
<div
className="flex-1 overflow-y-auto p-4 scroll-smooth"
style={{ display: activeTab === 'chat' ? 'block' : 'none' }}
ref={messageDivRef}
>
<MessageList
messages={messages}
latestInput={latestInput}
@@ -550,7 +646,10 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
/>
</div>
<div className="border-t px-4 py-3 border-gray-200 bg-white sticky bottom-0">
<div
className="border-t px-4 py-3 border-gray-200 bg-white sticky bottom-0"
style={{ display: activeTab === 'chat' ? 'block' : 'none' }}
>
{selectedNode && (
<SelectedNodeInfo
nodeInfo={selectedNode}
@@ -573,6 +672,14 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
onModelChange={setSelectedModel}
/>
</div>
{/* ParameterDebugTab - Always mounted but conditionally displayed */}
<div
className="flex-1 flex flex-col"
style={{ display: activeTab === 'parameter-debug' ? 'flex' : 'none' }}
>
{parameterDebugTabComponent}
</div>
</div>
</div>
);