Merge pull request #43 from AIDC-AI/parameter_debug_dev
Parameter debug
This commit is contained in:
Vendored
-6
@@ -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};
|
||||
Vendored
+6
@@ -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
@@ -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};
|
||||
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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 _};
|
||||
|
||||
Vendored
+37
-37
File diff suppressed because one or more lines are too long
+131
File diff suppressed because one or more lines are too long
-130
File diff suppressed because one or more lines are too long
+1
-1
@@ -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]
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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:
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user