feat: genlab optimize v2

This commit is contained in:
ai-business-hql
2025-05-15 16:22:40 +08:00
parent 426fd308fd
commit 4e23e8a709
34 changed files with 2492 additions and 952 deletions
@@ -1,6 +1,6 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-CjDaET6O.js","copilot_web/input.js","copilot_web/vendor-markdown-Dek94WS0.js","copilot_web/vendor-react-V04_Axys.js","copilot_web/message-components-DHkIj9qD.js","copilot_web/assets/input-0GzdDP_m.css","copilot_web/fonts.css"].map(path => {
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-DP_exkAV.js","copilot_web/input.js","copilot_web/vendor-markdown-Dek94WS0.js","copilot_web/vendor-react-V04_Axys.js","copilot_web/message-components-DHkIj9qD.js","copilot_web/assets/input-T2ACqglr.css","copilot_web/fonts.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 a}from"./input.js";import{j as t}from"./vendor-markdown-Dek94WS0.js";import{r,R as i}from"./vendor-react-V04_Axys.js";import{C as n}from"./message-components-DHkIj9qD.js";/* empty css */const o={EXPLAIN_NODE:"copilot:explain-node"},d=i.lazy(()=>a(()=>import("./workflowChat-CjDaET6O.js").then(e=>e.w),__vite__mapDeps([0,1,2,3,4,5,6])).then(e=>({default:e.default})));function p(){const[e,s]=r.useState(!1);return r.useEffect(()=>{const l=()=>{s(!0)};return window.addEventListener(o.EXPLAIN_NODE,l),()=>window.removeEventListener(o.EXPLAIN_NODE,l)},[]),t.jsx(n,{children:t.jsx("div",{className:"h-full w-full flex flex-col",children:t.jsx(r.Suspense,{fallback:t.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:t.jsx(d,{visible:!0,triggerUsage:e,onUsageTriggered:()=>s(!1)})})})})}export{p as default};
import{_ as a}from"./input.js";import{j as t}from"./vendor-markdown-Dek94WS0.js";import{r,R as i}from"./vendor-react-V04_Axys.js";import{C as n}from"./message-components-DHkIj9qD.js";/* empty css */const o={EXPLAIN_NODE:"copilot:explain-node"},d=i.lazy(()=>a(()=>import("./workflowChat-DP_exkAV.js").then(e=>e.w),__vite__mapDeps([0,1,2,3,4,5,6])).then(e=>({default:e.default})));function p(){const[e,s]=r.useState(!1);return r.useEffect(()=>{const l=()=>{s(!0)};return window.addEventListener(o.EXPLAIN_NODE,l),()=>window.removeEventListener(o.EXPLAIN_NODE,l)},[]),t.jsx(n,{children:t.jsx("div",{className:"h-full w-full flex flex-col",children:t.jsx(r.Suspense,{fallback:t.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:t.jsx(d,{visible:!0,triggerUsage:e,onUsageTriggered:()=>s(!1)})})})})}export{p as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,2 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/App-Dp5pJunx.js","copilot_web/vendor-markdown-Dek94WS0.js","copilot_web/vendor-react-V04_Axys.js","copilot_web/message-components-DHkIj9qD.js","copilot_web/fonts.css"])))=>i.map(i=>d[i]);
import{j as s}from"./vendor-markdown-Dek94WS0.js";import{a as w,R as p,r as x}from"./vendor-react-V04_Axys.js";import{w as v,a as g}from"./message-components-DHkIj9qD.js";/* empty css */const R="modulepreload",b=function(e){return"/"+e},m={},C=function(c,a,P){let d=Promise.resolve();if(a&&a.length>0){document.getElementsByTagName("link");const r=document.querySelector("meta[property=csp-nonce]"),t=r?.nonce||r?.getAttribute("nonce");d=Promise.allSettled(a.map(o=>{if(o=b(o),o in m)return;m[o]=!0;const i=o.endsWith(".css"),h=i?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${o}"]${h}`))return;const n=document.createElement("link");if(n.rel=i?"stylesheet":R,i||(n.as="script"),n.crossOrigin="",n.href=o,t&&n.setAttribute("nonce",t),document.head.appendChild(n),i)return new Promise((y,E)=>{n.addEventListener("load",y),n.addEventListener("error",()=>E(new Error(`Unable to preload CSS for ${o}`)))})}))}function u(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"&&u(t.reason);return c().catch(u)})};var l={},f=w;l.createRoot=f.createRoot,l.hydrateRoot=f.hydrateRoot;const S=p.lazy(()=>C(async()=>{const{default:e}=await import("./App-Dp5pJunx.js");return{default:e}},__vite__mapDeps([0,1,2,3,4])).then(({default:e})=>({default:e})));function j(){return new Promise(e=>{if(document.body)return e(document.body);document.addEventListener("DOMContentLoaded",()=>{e(document.body)})})}j().then(()=>v()).then(()=>{g.extensionManager.registerSidebarTab({id:"comfyui-copilot",icon:"cc-icon-logo",title:"ComfyUI Copilot",tooltip:"ComfyUI Copilot",type:"custom",render:e=>{const c=document.createElement("div");c.id="comfyui-copilot-plugin",c.className="h-full w-full flex flex-col",e.style.height="100%",e.appendChild(c),l.createRoot(c).render(s.jsx(p.StrictMode,{children:s.jsx(x.Suspense,{fallback:s.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:s.jsx(S,{})})}))}})});export{C as _};
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/App-C_jxBl2D.js","copilot_web/vendor-markdown-Dek94WS0.js","copilot_web/vendor-react-V04_Axys.js","copilot_web/message-components-DHkIj9qD.js","copilot_web/fonts.css"])))=>i.map(i=>d[i]);
import{j as s}from"./vendor-markdown-Dek94WS0.js";import{a as w,R as p,r as x}from"./vendor-react-V04_Axys.js";import{w as v,a as g}from"./message-components-DHkIj9qD.js";/* empty css */const R="modulepreload",b=function(e){return"/"+e},m={},C=function(c,a,P){let d=Promise.resolve();if(a&&a.length>0){document.getElementsByTagName("link");const r=document.querySelector("meta[property=csp-nonce]"),t=r?.nonce||r?.getAttribute("nonce");d=Promise.allSettled(a.map(o=>{if(o=b(o),o in m)return;m[o]=!0;const i=o.endsWith(".css"),h=i?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${o}"]${h}`))return;const n=document.createElement("link");if(n.rel=i?"stylesheet":R,i||(n.as="script"),n.crossOrigin="",n.href=o,t&&n.setAttribute("nonce",t),document.head.appendChild(n),i)return new Promise((y,E)=>{n.addEventListener("load",y),n.addEventListener("error",()=>E(new Error(`Unable to preload CSS for ${o}`)))})}))}function u(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"&&u(t.reason);return c().catch(u)})};var l={},f=w;l.createRoot=f.createRoot,l.hydrateRoot=f.hydrateRoot;const S=p.lazy(()=>C(async()=>{const{default:e}=await import("./App-C_jxBl2D.js");return{default:e}},__vite__mapDeps([0,1,2,3,4])).then(({default:e})=>({default:e})));function j(){return new Promise(e=>{if(document.body)return e(document.body);document.addEventListener("DOMContentLoaded",()=>{e(document.body)})})}j().then(()=>v()).then(()=>{g.extensionManager.registerSidebarTab({id:"comfyui-copilot",icon:"cc-icon-logo",title:"ComfyUI Copilot",tooltip:"ComfyUI Copilot",type:"custom",render:e=>{const c=document.createElement("div");c.id="comfyui-copilot-plugin",c.className="h-full w-full flex flex-col",e.style.height="100%",e.appendChild(c),l.createRoot(c).render(s.jsx(p.StrictMode,{children:s.jsx(x.Suspense,{fallback:s.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:s.jsx(S,{})})}))}})});export{C as _};
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -1242,7 +1242,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001684",
"version": "1.0.30001718",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"dev": true,
"funding": [
{
@@ -1257,8 +1259,7 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
]
},
"node_modules/ccount": {
"version": "2.0.1",
+7 -13
View File
@@ -69,36 +69,30 @@ export function ChatHeader({
return (
<>
<div className="flex items-center justify-between border-b px-4 py-3
<div className="flex items-center justify-between border-b px-3 py-2
bg-white border-gray-200 sticky top-2 z-10">
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1">
<img
src={logoImage}
alt="ComfyUI-Copilot Logo"
className="h-10 w-10 -ml-1"
className="h-7 w-7 -ml-1"
/>
<h3 className="text-[16px] font-medium text-gray-800">{title}</h3>
<h3 className="text-[14px] 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"
>
<CogIcon className="h-5 w-5" />
<CogIcon className="h-4 w-4" />
</button>
</div>
<div className="flex items-center gap-1">
<button
className={`inline-flex bg-white border-none items-center justify-center rounded-md p-2
className={`inline-flex bg-white border-none items-center justify-center rounded-md p-1.5
${hasMessages ? 'text-gray-500 hover:bg-gray-100' : 'text-gray-300 cursor-not-allowed'}`}
disabled={!hasMessages}
onClick={onClear}>
<TrashIcon className="h-5 w-5" />
<TrashIcon className="h-4 w-4" />
</button>
{/* <button
className="inline-flex bg-white border-none items-center justify-center rounded-md p-2
text-gray-500 hover:bg-gray-100"
onClick={onClose}>
<XIcon className="h-5 w-5" />
</button> */}
</div>
</div>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
import React, { useState, useEffect } from 'react';
import { ParameterDebugInterface as OriginalParameterDebugInterface } from './ParameterDebugInterfaceNew';
import { ParameterDebugInterfaceProps } from './types/parameterDebugTypes';
import { useChatContext } from '../../context/ChatContext';
import { loadStateFromLocalStorage, clearStateFromLocalStorage } from './utils/localStorageUtils';
/**
* Enhanced version of ParameterDebugInterface with improved architecture
*
* This component:
* 1. Handles loading/persisting state to/from localStorage
* 2. Manages context interactions
* 3. Delegates rendering to the original component
*/
export const ParameterDebugInterface: React.FC<ParameterDebugInterfaceProps> = (props) => {
const { dispatch } = useChatContext();
const [isInitialized, setIsInitialized] = useState(false);
// Handle initialization - load saved state if available
useEffect(() => {
if (props.visible && !isInitialized) {
// Attempt to load saved state
const savedState = loadStateFromLocalStorage();
if (savedState) {
// Update context with screen state if available
if (savedState.screenState) {
dispatch({
type: 'SET_SCREEN_STATE',
payload: {
currentScreen: savedState.currentScreen,
isProcessing: savedState.isProcessing,
isCompleted: savedState.isCompleted
}
});
}
}
setIsInitialized(true);
}
}, [props.visible, isInitialized, dispatch]);
// Clean up when component unmounts
useEffect(() => {
return () => {
if (!props.visible) {
dispatch({ type: 'SET_SCREEN_STATE', payload: null });
}
};
}, [props.visible, dispatch]);
// The original component handles most of the actual work
return <OriginalParameterDebugInterface {...props} />;
};
+45
View File
@@ -0,0 +1,45 @@
# Parameter Debug Interface
This directory contains the Parameter Debug Interface components for ComfyUI Copilot.
## Directory Structure
- `ParameterDebugInterfaceNew.tsx` - Original large component (refactored to use extracted utilities)
- `ParameterDebugInterfaceV2.tsx` - Enhanced wrapper component with better architecture
- `index.ts` - Central export file for all components and utilities
- `types/` - Type definitions extracted from the original component
- `parameterDebugTypes.ts` - Common type definitions
- `utils/` - Utility functions extracted from the original component
- `localStorageUtils.ts` - LocalStorage related utilities
- `textInputUtils.ts` - Text input handling utilities
- `parameterUtils.ts` - Parameter processing utilities
- `interfaceUtils.ts` - UI interaction utilities
- `searchUtils.ts` - Search and filter utilities
- `styles/` - Style definitions
- `highlightPulseStyle.ts` - CSS animation styles
- `screens/` - Screen components used by the interface
- `modals/` - Modal components used by the interface
## Refactoring Strategy
The original `ParameterDebugInterfaceNew.tsx` file was very large and complex. Our strategy was to:
1. Extract independent parts (types, utilities, styles) without changing the core logic
2. Create a wrapper (`ParameterDebugInterfaceV2.tsx`) that uses the original component but leverages the extracted parts
3. Refactor the original component to use the extracted utility functions
4. Update the export structure to expose a clean API
## Benefits of the New Architecture
1. **Modularity**: Core functions are organized by purpose in separate files
2. **Reusability**: Utilities can be easily reused across components
3. **Maintainability**: Smaller files are easier to understand and modify
4. **Performance**: Cleaner code structure may lead to better performance
5. **Testing**: Isolated utilities are easier to test
## Future Improvements
1. Continue splitting functionality into more specialized files
2. Convert screen components to use the extracted utilities
3. Add proper typing to all function parameters
4. Implement unit tests for utilities
+15
View File
@@ -0,0 +1,15 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
// Re-export components
export { ParameterDebugInterface } from './ParameterDebugInterfaceV2';
// Re-export types
export * from './types/parameterDebugTypes';
// Re-export utilities
export * from './utils/localStorageUtils';
export * from './utils/textInputUtils';
export * from './utils/parameterUtils';
export * from './utils/interfaceUtils';
export * from './utils/searchUtils';
+85 -9
View File
@@ -1,4 +1,3 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
@@ -7,33 +6,110 @@ import React from 'react';
interface ImageModalProps {
visible: boolean;
imageUrl: string;
params?: { [key: string]: any };
onClose: (event?: React.MouseEvent) => void;
}
export const ImageModal: React.FC<ImageModalProps> = ({
visible,
imageUrl,
params,
onClose
}) => {
if (!visible) return null;
// 处理参数信息格式化显示
const formatParams = () => {
if (!params || !params.nodeParams) return null;
const nodeElements = Object.entries(params.nodeParams).map(([nodeId, nodeParams]) => {
// 获取节点名称 (如果存在)
const nodeName = params.nodeNames?.[nodeId] || `Node ${nodeId}`;
// 渲染每个参数
const paramElements = Object.entries(nodeParams as Record<string, any>)
.filter(([_, value]) => typeof value !== 'object' || value === null) // 过滤掉复杂对象类型,但允许null
.map(([paramName, value]) => (
<div key={paramName} className="pl-4 text-sm py-1 flex">
<span className="text-gray-600 dark:text-gray-400 mr-2">-</span>
<span className="font-medium mr-1">{paramName}:</span>
<span className="text-gray-700 dark:text-gray-300 break-all">{String(value)}</span>
</div>
));
return (
<div key={nodeId} className="mb-4">
<div className="font-semibold text-pink-600 dark:text-pink-400 text-base py-1 border-b border-gray-200 dark:border-gray-700 mb-2">
{nodeName}
</div>
{paramElements.length > 0 ? (
paramElements
) : (
<div className="pl-4 text-sm text-gray-500 italic">nodeParams is null</div>
)}
</div>
);
});
return (
<div className="overflow-y-auto max-h-full">
{nodeElements.length > 0 ? nodeElements : (
<div className="text-sm text-gray-500 italic">nodeParams is null</div>
)}
</div>
);
};
return (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50" onClick={onClose}>
<div className="bg-white dark:bg-gray-800 rounded-xl p-2 max-w-4xl max-h-[90vh] relative shadow-2xl" onClick={(e) => e.stopPropagation()}>
<div
className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl flex flex-row w-[90%] max-w-6xl max-h-[90vh] relative m-4 overflow-hidden"
onClick={(e) => e.stopPropagation()}
style={{ display: 'flex', flexDirection: 'row' }} // 强制使用横向flex布局
>
<button
className="absolute top-2 right-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 p-1"
className="absolute top-2 right-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 p-1 z-10"
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 className="w-[65%] border-r border-gray-200 dark:border-gray-700">
<div className="p-4 h-full flex items-center justify-center">
<img
src={imageUrl}
alt="Enlarged image"
className="max-w-full max-h-[calc(90vh-2rem)] object-contain"
/>
</div>
</div>
{/* 右侧参数信息 */}
<div className="w-[35%] h-full overflow-hidden flex flex-col">
<div className="p-4 flex-1 overflow-y-auto">
<h3 className="text-lg font-medium text-gray-800 dark:text-gray-200 mb-4">parameters</h3>
{/* 显示常规参数 */}
{params && !params.nodeParams && Object.entries(params)
.filter(([paramName, value]) =>
paramName !== 'nodeParams' &&
typeof value !== 'object'
)
.map(([paramName, value]) => (
<div key={paramName} className="py-1 text-sm text-gray-600 dark:text-gray-300">
<span className="font-medium">{paramName}:</span> {String(value)}
</div>
))
}
{/* 显示节点参数 */}
<div className="text-sm text-gray-600 dark:text-gray-300">
{formatParams()}
</div>
</div>
</div>
</div>
</div>
@@ -151,7 +151,7 @@ export const ConfigureParameterScreen: React.FC<ConfigureParameterScreenProps> =
<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>
<h3 className="text-base font-medium text-gray-800">Set test values</h3>
<p className="text-xs text-gray-500">Configure the test values for each parameter</p>
</div>
<button
@@ -125,7 +125,7 @@ export const ConfirmConfigurationScreen: React.FC<ConfirmConfigurationScreenProp
<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>
<h3 className="text-base font-medium text-gray-800">Conform combinations</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>
@@ -238,7 +238,7 @@ export const ConfirmConfigurationScreen: React.FC<ConfirmConfigurationScreenProp
}`}
disabled={imageNodeIds.length > 1 && !selectedImageNodeId}
>
Start Generation
Generate & Compare
</button>
</div>
</div>
@@ -0,0 +1,153 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
import React from 'react';
import { ImageModal } from '../modals/ImageModal';
import { GeneratedImage } from '../types/parameterDebugTypes';
import { HistoryItem, formatDate } from '../utils/historyUtils';
interface HistoryItemScreenProps {
historyItem: HistoryItem;
selectedImageIndex: number | null;
handleSelectImage: (index: number, event?: React.MouseEvent) => void;
handleClose: (event?: React.MouseEvent) => void;
currentPage: number;
handlePageChange: (newPage: number, event?: React.MouseEvent) => void;
imagesPerPage: number;
modalVisible: boolean;
modalImageUrl: string;
modalImageParams: { [key: string]: any } | null;
openImageModal: (imageUrl: string, params: { [key: string]: any }, event: React.MouseEvent) => void;
closeImageModal: (event?: React.MouseEvent) => void;
}
export const HistoryItemScreen: React.FC<HistoryItemScreenProps> = ({
historyItem,
selectedImageIndex,
handleSelectImage,
handleClose,
currentPage,
handlePageChange,
imagesPerPage,
modalVisible,
modalImageUrl,
modalImageParams,
openImageModal,
closeImageModal
}) => {
const { timestamp, nodeName, generatedImages } = historyItem;
// 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">{nodeName}</h3>
<p className="text-xs text-gray-500">{formatDate(timestamp)} • {generatedImages.length} images</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>
{/* Image Modal */}
<ImageModal
visible={modalVisible}
imageUrl={modalImageUrl}
params={modalImageParams || {}}
onClose={closeImageModal}
/>
<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, image.params, 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>
);
})}
</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>
);
};
@@ -0,0 +1,76 @@
/*
* @Author: ai-business-hql qingli.hql@alibaba-inc.com
* @Date: 2025-05-15 15:30:33
* @LastEditors: ai-business-hql qingli.hql@alibaba-inc.com
* @LastEditTime: 2025-05-15 16:13:38
* @FilePath: /comfyui_copilot/ui/src/components/debug/screens/HistoryScreen.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.
import React from 'react';
import { HistoryItem, formatDate, formatNodeNameWithParams } from '../utils/historyUtils';
interface HistoryScreenProps {
historyItems: HistoryItem[];
onViewHistoryItem: (historyItem: HistoryItem) => void;
onClose: () => void;
}
export const HistoryScreen: React.FC<HistoryScreenProps> = ({
historyItems,
onViewHistoryItem,
onClose
}) => {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 flex flex-col h-full">
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<div>
<h3 className="text-base font-medium text-gray-800">Generation History</h3>
<p className="text-xs text-gray-500">Last {historyItems.length} generation runs</p>
</div>
<button
className="text-gray-400 hover:text-gray-600"
onClick={onClose}
>
<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>
{historyItems.length === 0 ? (
<div className="flex-1 flex items-center justify-center text-gray-500 text-sm">
No history items found
</div>
) : (
<div className="flex-1 overflow-y-auto">
{historyItems.map((item) => (
<div
key={item.id}
className="border border-gray-200 rounded-lg mb-3 overflow-hidden"
>
<div className="bg-gray-50 py-2 px-3 flex items-center justify-between border-b">
<div className="text-[14px] text-gray-600">
{formatDate(item.timestamp)}
</div>
<button
onClick={() => onViewHistoryItem(item)}
className="px-3 py-1 text-xs bg-pink-200 text-pink-700 rounded-md hover:bg-pink-300 transition-colors"
>
view
</button>
</div>
<div className="p-3">
<div className="text-[10px] font-medium text-gray-700">
{formatNodeNameWithParams(item.nodeName, item.params)}
</div>
</div>
</div>
))}
</div>
)}
</div>
);
};
@@ -1,8 +1,8 @@
/*
* @Author: ai-business-hql ai.bussiness.hql@gmail.com
* @Date: 2025-03-31 18:29:53
* @LastEditors: ai-business-hql ai.bussiness.hql@gmail.com
* @LastEditTime: 2025-04-09 11:39:45
* @LastEditors: ai-business-hql qingli.hql@alibaba-inc.com
* @LastEditTime: 2025-05-15 16:18:09
* @FilePath: /comfyui_copilot/ui/src/components/debug/screens/InitialScreen.tsx
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@@ -38,8 +38,8 @@ export const InitialScreen: React.FC<InitialScreenProps> = ({
<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>
<h3 className="text-base font-medium text-gray-800">Select parameters</h3>
<p className="text-xs text-gray-500">multiple selections supported</p>
</div>
<button
className="text-gray-400 hover:text-gray-600"
@@ -3,6 +3,7 @@
import React from 'react';
import { app } from '../../../utils/comfyapp';
import { ImageModal } from '../modals/ImageModal';
interface GeneratedImage {
url: string;
@@ -22,7 +23,8 @@ interface ResultGalleryScreenProps {
notificationVisible: boolean;
modalVisible: boolean;
modalImageUrl: string;
openImageModal: (imageUrl: string, event: React.MouseEvent) => void;
modalImageParams: { [key: string]: any } | null;
openImageModal: (imageUrl: string, params: { [key: string]: any }, event: React.MouseEvent) => void;
closeImageModal: (event?: React.MouseEvent) => void;
}
@@ -39,6 +41,7 @@ export const ResultGalleryScreen: React.FC<ResultGalleryScreenProps> = ({
notificationVisible,
modalVisible,
modalImageUrl,
modalImageParams,
openImageModal,
closeImageModal
}) => {
@@ -82,27 +85,12 @@ export const ResultGalleryScreen: React.FC<ResultGalleryScreenProps> = ({
)}
{/* 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 object-contain w-auto h-auto"
/>
</div>
</div>
</div>
)}
<ImageModal
visible={modalVisible}
imageUrl={modalImageUrl}
params={modalImageParams || {}}
onClose={closeImageModal}
/>
<div className="grid grid-cols-3 gap-4 mb-6">
{currentImages.map((image, indexOnPage) => {
@@ -116,7 +104,7 @@ export const ResultGalleryScreen: React.FC<ResultGalleryScreenProps> = ({
{/* 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)}
onClick={(e) => openImageModal(image.url, image.params, 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" />
@@ -129,18 +117,6 @@ export const ResultGalleryScreen: React.FC<ResultGalleryScreenProps> = ({
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>
);
})}
@@ -195,13 +171,7 @@ export const ResultGalleryScreen: React.FC<ResultGalleryScreenProps> = ({
</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>
<div className="mt-6 flex justify-end">
<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"
@@ -0,0 +1,33 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
// Add CSS for the highlight pulse effect
export const highlightPulseStyle = `
@keyframes highlight-pulse {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5);
}
70% {
box-shadow: 0 0 0 15px rgba(59, 130, 246, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
.highlight-pulse {
animation: highlight-pulse 1.5s infinite;
border: 2px solid #3b82f6;
}
@keyframes fade-in-out {
0% { opacity: 0; transform: translateY(-20px); }
10% { opacity: 1; transform: translateY(0); }
90% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-20px); }
}
.animate-fade-in-out {
animation: fade-in-out 3s forwards;
}
`;
@@ -0,0 +1,34 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
// Interface for generated images
export interface GeneratedImage {
url: string;
params: { [key: string]: any };
}
// Interface for parameter debug interface props
export interface ParameterDebugInterfaceProps {
selectedNodes: any[];
visible: boolean;
onClose?: () => void;
}
// Interface for dropdown state
export interface DropdownState {
isOpen: boolean;
x: number;
y: number;
}
// Interface for mapping widget parameters
export interface WidgetParamConf {
[key: string]: any;
}
// Interface for uploaded image
export interface UploadedImage {
id: string;
file: File;
preview: string;
}
@@ -0,0 +1,146 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
import { WorkflowChatAPI } from '../../../apis/workflowChatApi';
/**
* 处理AI文本生成
*/
export const handleAiWriting = async (
aiWritingModalText: string,
task_id: string,
setAiWritingLoading: React.Dispatch<React.SetStateAction<boolean>>,
setAiWritingError: React.Dispatch<React.SetStateAction<string | null>>,
setAiGeneratedTexts: React.Dispatch<React.SetStateAction<string[]>>,
setAiSelectedTexts: React.Dispatch<React.SetStateAction<{[key: string]: boolean}>>
) => {
if (!aiWritingModalText.trim()) {
setAiWritingError("Please enter some text to generate variations");
return;
}
setAiWritingLoading(true);
setAiWritingError(null);
setAiGeneratedTexts([]);
setAiSelectedTexts({});
try {
const generatedTexts = await WorkflowChatAPI.generateSDPrompts(aiWritingModalText);
setAiGeneratedTexts(generatedTexts);
// Send tracking event
WorkflowChatAPI.trackEvent({
event_type: 'prompt_generate',
message_type: 'parameter_debug',
message_id: task_id,
data: {
input_text: aiWritingModalText,
generated_texts: generatedTexts
}
});
// Pre-select all generated texts
const newSelectedTexts: {[key: string]: boolean} = {};
generatedTexts.forEach((text, index) => {
newSelectedTexts[`text${index+1}`] = false;
});
setAiSelectedTexts(newSelectedTexts);
} catch (error) {
console.error("Error generating text:", error);
setAiWritingError("Failed to generate text variations. Please try again.");
} finally {
setAiWritingLoading(false);
}
};
/**
* 处理文本选择切换
*/
export const toggleTextSelection = (
textKey: string,
setAiSelectedTexts: React.Dispatch<React.SetStateAction<{[key: string]: boolean}>>
) => {
setAiSelectedTexts(prev => ({
...prev,
[textKey]: !prev[textKey]
}));
};
/**
* 添加选中的文本到文本输入列表
*/
export const addSelectedTexts = (
aiWritingNodeId: string,
aiWritingParamName: string,
aiSelectedTexts: {[key: string]: boolean},
aiGeneratedTexts: string[],
aiWritingModalText: string,
task_id: string,
textInputs: {[nodeId_paramName: string]: string[]},
setTextInputs: React.Dispatch<React.SetStateAction<{[nodeId_paramName: string]: string[]}>>,
updateParamTestValues: (nodeId: string, paramName: string, values: any[]) => void,
setAiWritingModalVisible: React.Dispatch<React.SetStateAction<boolean>>
) => {
const textKey = `${aiWritingNodeId}_${aiWritingParamName}`;
const selectedTexts = Object.entries(aiSelectedTexts)
.filter(([_, isSelected]) => isSelected)
.map(([key]) => {
const index = parseInt(key.replace('text', '')) - 1;
return aiGeneratedTexts[index];
});
if (selectedTexts.length === 0) {
return;
}
setTextInputs(prev => {
const currentTexts = [...(prev[textKey] || [])];
return {
...prev,
[textKey]: [...currentTexts, ...selectedTexts]
};
});
// Send tracking event
WorkflowChatAPI.trackEvent({
event_type: 'prompt_apply',
message_type: 'parameter_debug',
message_id: task_id,
data: {
input_text: aiWritingModalText,
generated_texts: aiGeneratedTexts,
selected_texts: selectedTexts
}
});
// Also update paramTestValues
const updatedTexts = [...(textInputs[textKey] || []), ...selectedTexts];
updateParamTestValues(aiWritingNodeId, aiWritingParamName, updatedTexts);
// Close the modal
setAiWritingModalVisible(false);
};
/**
* 打开AI写作模态框
*/
export const openAiWritingModal = (
nodeId: string,
paramName: string,
setAiWritingModalVisible: React.Dispatch<React.SetStateAction<boolean>>,
setAiWritingNodeId: React.Dispatch<React.SetStateAction<string>>,
setAiWritingParamName: React.Dispatch<React.SetStateAction<string>>,
setAiWritingModalText: React.Dispatch<React.SetStateAction<string>>,
setAiGeneratedTexts: React.Dispatch<React.SetStateAction<string[]>>,
setAiSelectedTexts: React.Dispatch<React.SetStateAction<{[key: string]: boolean}>>,
setAiWritingLoading: React.Dispatch<React.SetStateAction<boolean>>,
setAiWritingError: React.Dispatch<React.SetStateAction<string | null>>
) => {
setAiWritingModalVisible(true);
setAiWritingNodeId(nodeId);
setAiWritingParamName(paramName);
setAiWritingModalText('');
setAiGeneratedTexts([]);
setAiSelectedTexts({});
setAiWritingLoading(false);
setAiWritingError(null);
};
@@ -0,0 +1,148 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
import { GeneratedImage } from '../types/parameterDebugTypes';
import { generateUUID } from '../../../utils/uuid';
// Constants
export const PARAM_DEBUG_HISTORY_KEY = 'parameter_debug_history';
export const MAX_HISTORY_ITEMS = 30;
// Interface for history item
export interface HistoryItem {
id: string;
timestamp: number;
nodeName: string;
params: {[key: string]: any};
generatedImages: GeneratedImage[];
totalCount: number;
workflow?: any; // Optional workflow data
}
/**
* Save a new history item to localStorage
*/
export const saveHistoryItem = async (
nodeName: string,
params: {[key: string]: any},
generatedImages: GeneratedImage[],
totalCount: number,
workflow?: any
) => {
try {
// Get existing history
const history = loadHistoryItems();
// Create new history item
const newItem: HistoryItem = {
id: generateUUID(),
timestamp: Date.now(),
nodeName,
params,
generatedImages,
totalCount,
workflow
};
// Add new item to the beginning of the array (newest first)
history.unshift(newItem);
// Keep only the most recent MAX_HISTORY_ITEMS
if (history.length > MAX_HISTORY_ITEMS) {
history.splice(MAX_HISTORY_ITEMS);
}
// Save back to localStorage
localStorage.setItem(PARAM_DEBUG_HISTORY_KEY, JSON.stringify(history));
return newItem;
} catch (error) {
console.error('Error saving parameter debug history:', error);
return null;
}
};
/**
* Load all history items from localStorage
*/
export const loadHistoryItems = (): HistoryItem[] => {
try {
const savedHistory = localStorage.getItem(PARAM_DEBUG_HISTORY_KEY);
if (savedHistory) {
return JSON.parse(savedHistory);
}
} catch (error) {
console.error('Error loading parameter debug history:', error);
}
return [];
};
/**
* Get a specific history item by ID
*/
export const getHistoryItemById = (id: string): HistoryItem | null => {
try {
const history = loadHistoryItems();
return history.find(item => item.id === id) || null;
} catch (error) {
console.error('Error getting history item:', error);
return null;
}
};
/**
* Clear all history items
*/
export const clearHistory = () => {
try {
localStorage.removeItem(PARAM_DEBUG_HISTORY_KEY);
} catch (error) {
console.error('Error clearing parameter debug history:', error);
}
};
/**
* Format date for display
*/
export const formatDate = (timestamp: number): string => {
const date = new Date(timestamp);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
};
/**
* Format node name and parameters for display
*/
export const formatNodeNameWithParams = (
nodeName: string,
params: {[key: string]: any}
): string => {
// Check if we have nodeParams structure which contains node-to-params mapping
if (params.nodeParams) {
const nodeParamsEntries = Object.entries(params.nodeParams);
// Format each node with its parameters
const nodeStrings = nodeParamsEntries.map(([nodeId, nodeParams]) => {
// Get node name from selectedNodeInfoMap if available
let displayNodeName = nodeName;
if (params.selectedNodeInfoMap && params.selectedNodeInfoMap[nodeId]) {
displayNodeName = params.selectedNodeInfoMap[nodeId];
}
// Get parameter names for this node (up to 3)
const paramNames = Object.keys(nodeParams as object).slice(0, 3);
// Format as node_name<param1,param2,param3>
return `${displayNodeName}<${paramNames.join(',')}>`;
});
// Join all nodes with semicolons
return nodeStrings.join(';');
}
// Fallback to basic format if nodeParams not available
// Get up to 3 parameter names (not values)
const paramKeys = Object.keys(params).filter(key => key !== 'nodeParams' && key !== 'selectedNodeInfoMap').slice(0, 3);
// Format as node_name<param1,param2,param3>
return `${nodeName}<${paramKeys.join(',')}>`;
};
@@ -0,0 +1,333 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
import { WorkflowChatAPI } from '../../../apis/workflowChatApi';
import { queuePrompt, getOutputImageByPromptId, getOnlyOneImageNode } from '../../../utils/queuePrompt';
import { generateDynamicParams } from './parameterUtils';
import { saveHistoryItem } from './historyUtils';
/**
* 处理启动图像生成过程
*/
export const handleStartGeneration = async (
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}},
task_id: string,
setIsProcessing: React.Dispatch<React.SetStateAction<boolean>>,
setCompletedCount: React.Dispatch<React.SetStateAction<number>>,
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>,
setGeneratedImages: React.Dispatch<React.SetStateAction<any[]>>,
setIsCompleted: React.Dispatch<React.SetStateAction<boolean>>,
setCurrentPage: React.Dispatch<React.SetStateAction<number>>,
pollingSessionIdRef: React.MutableRefObject<string | null>,
pollingTimeoutRef: React.MutableRefObject<NodeJS.Timeout | null>,
generateParameterCombinations: (paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}}) => any[],
app: any,
event?: React.MouseEvent,
selectedNodeId?: number
) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
// Clean up any existing polling
cleanupPolling(pollingTimeoutRef, pollingSessionIdRef);
// Get parameter combinations
const paramCombinations = generateParameterCombinations(paramTestValues);
const totalCombinations = paramCombinations.length;
// Check if total runs exceed 100
if (totalCombinations > 100) {
setErrorMessage(`Cannot generate more than 100 images at once (currently: ${totalCombinations} images)`);
return;
}
setErrorMessage(null);
setIsProcessing(true);
setCompletedCount(0);
console.log("Generated parameter combinations:", paramCombinations);
// Get workflow data for history
let workflowData = null;
try {
workflowData = (await app.graphToPrompt()).output;
} catch (error) {
console.error("Error capturing workflow data:", error);
}
// Send tracking event
WorkflowChatAPI.trackEvent({
event_type: 'start_generation',
message_type: 'parameter_debug',
message_id: task_id,
data: {
workflow: workflowData,
all_params: paramTestValues,
count: totalCombinations
}
});
// If we have no combinations, show error and return
if (paramCombinations.length === 0) {
setErrorMessage("No valid parameter combinations found. Please check your parameter selections.");
setIsProcessing(false);
return;
}
// Get node ID to use for showing images - Use provided selectedNodeId if available
let showNodeId: number | null = selectedNodeId || null;
// If no selectedNodeId provided, try to get one automatically
if (showNodeId === null) {
const nodeId = getOnlyOneImageNode();
if (nodeId !== null) {
showNodeId = nodeId;
}
}
// If showNodeId is null, we need user selection (handled in ConfirmConfigurationScreen)
if (showNodeId === null) {
setErrorMessage("Please select a SaveImage or PreviewImage node first.");
setIsProcessing(false);
return;
}
// Get the actual selected node names for history record
// This will map node IDs to their actual node types/names
const selectedNodeInfoMap: {[nodeId: string]: string} = {};
// Get node IDs from paramTestValues keys (these are the nodes user selected for testing)
Object.keys(paramTestValues).forEach((nodeId) => {
// Get node type from app.graph
const node = app.graph._nodes_by_id[nodeId];
if (node) {
// Store node type/class as name
selectedNodeInfoMap[nodeId] = node.type || node.comfyClass || "Unknown Node";
}
});
const prompt_ids: string[] = [];
try {
// Generate a unique session ID
const sessionId = Date.now().toString();
pollingSessionIdRef.current = sessionId;
// Each combination represents all parameters for a single image generation
for (const combination of paramCombinations) {
const response = await queuePrompt(combination);
if(response.prompt_id) {
prompt_ids.push(response.prompt_id);
} else {
console.error("Failed to get prompt_id from response:", response);
prompt_ids.push("");
}
}
// Create an array to track images and their parameters
const newImages: any[] = Array(paramCombinations.length).fill(null).map((_, i) => ({
url: `https://source.unsplash.com/random/300x300?sig=${Math.random()}`, // Default placeholder
params: generateDynamicParams(paramTestValues, i)
}));
// Set initial images
setGeneratedImages(newImages);
// Track timeout
const startTime = Date.now();
const timeoutDuration = 5 * 60 * 1000; // 5 minutes
// Get primary selected node name for display
// We use the first node from paramTestValues as the primary node name
const primaryNodeId = Object.keys(paramTestValues)[0];
const primaryNodeName = selectedNodeInfoMap[primaryNodeId] || "Unknown Node";
// Start polling for images
pollForImages(
prompt_ids,
showNodeId,
newImages,
sessionId,
startTime,
timeoutDuration,
pollingSessionIdRef,
pollingTimeoutRef,
setGeneratedImages,
setCompletedCount,
setIsProcessing,
setIsCompleted,
setCurrentPage,
primaryNodeName,
paramTestValues,
totalCombinations,
workflowData,
selectedNodeInfoMap
);
} catch (error) {
console.error("Error generating images:", error);
setIsProcessing(false);
}
};
/**
* 处理图像轮询逻辑
*/
export const pollForImages = async (
prompt_ids: string[],
showNodeId: number,
newImages: any[],
sessionId: string,
startTime: number,
timeoutDuration: number,
pollingSessionIdRef: React.MutableRefObject<string | null>,
pollingTimeoutRef: React.MutableRefObject<NodeJS.Timeout | null>,
setGeneratedImages: React.Dispatch<React.SetStateAction<any[]>>,
setCompletedCount: React.Dispatch<React.SetStateAction<number>>,
setIsProcessing: React.Dispatch<React.SetStateAction<boolean>>,
setIsCompleted: React.Dispatch<React.SetStateAction<boolean>>,
setCurrentPage: React.Dispatch<React.SetStateAction<number>>,
nodeName?: string,
paramTestValues?: {[nodeId: string]: {[paramName: string]: any[]}},
totalCount?: number,
workflowData?: any,
selectedNodeInfoMap?: {[nodeId: string]: string}
) => {
// Check if this polling session is still active
if (pollingSessionIdRef.current !== sessionId) {
console.log("Another polling session has started, stopping this one");
return;
}
// Check if timeout has been reached
if (Date.now() - startTime > timeoutDuration) {
console.log("Timeout reached while waiting for images");
if (pollingSessionIdRef.current === sessionId) {
setIsProcessing(false);
setIsCompleted(true);
setCurrentPage(1);
// Save to history even if timeout occurred
if (newImages.length > 0 && nodeName && paramTestValues) {
// Use the first image's params which contains nodeParams with full structure
// This ensures we have the node-parameter relationships
const params = newImages[0].params;
// If we have selectedNodeInfoMap, add it to params for formatNodeNameWithParams to use
if (selectedNodeInfoMap) {
params.selectedNodeInfoMap = selectedNodeInfoMap;
}
// Save to history with current node name and parameters
saveHistoryItem(
nodeName,
params,
newImages,
totalCount || newImages.length,
workflowData
);
}
}
return;
}
let completedImagesCount = 0;
// Check each prompt id to see if images are ready
for (let i = 0; i < prompt_ids.length; i++) {
const promptId = prompt_ids[i];
if (!promptId) continue;
try {
// We've already verified showNodeId is not null at this point
const imageUrl = await getOutputImageByPromptId(promptId, Number(showNodeId));
if (imageUrl) {
// If we have an image URL, update in our array
newImages[i] = {
...newImages[i],
url: imageUrl
};
completedImagesCount++;
}
} catch (error) {
console.error(`Error fetching image for prompt ID ${promptId}:`, error);
}
}
// Update state with newest images
setGeneratedImages([...newImages]);
setCompletedCount(completedImagesCount);
// Check if all images are generated
if (completedImagesCount === prompt_ids.length) {
console.log("All images have been generated!");
if (pollingSessionIdRef.current === sessionId) {
setIsProcessing(false);
setIsCompleted(true);
setCurrentPage(1);
// Save to history when all images are completed
if (newImages.length > 0 && nodeName && paramTestValues) {
// Use the first image's params which contains nodeParams with full structure
// This ensures we have the node-parameter relationships
const params = newImages[0].params;
// If we have selectedNodeInfoMap, add it to params for formatNodeNameWithParams to use
if (selectedNodeInfoMap) {
params.selectedNodeInfoMap = selectedNodeInfoMap;
}
// Save to history with current node name and parameters
saveHistoryItem(
nodeName,
params,
newImages,
totalCount || newImages.length,
workflowData
);
}
}
return;
}
// Otherwise continue polling
if (pollingSessionIdRef.current === sessionId) {
pollingTimeoutRef.current = setTimeout(() => {
pollForImages(
prompt_ids,
showNodeId,
newImages,
sessionId,
startTime,
timeoutDuration,
pollingSessionIdRef,
pollingTimeoutRef,
setGeneratedImages,
setCompletedCount,
setIsProcessing,
setIsCompleted,
setCurrentPage,
nodeName,
paramTestValues,
totalCount,
workflowData,
selectedNodeInfoMap
);
}, 2000); // Poll every 2 seconds
}
};
/**
* 清理轮询超时
*/
export const cleanupPolling = (
pollingTimeoutRef: React.MutableRefObject<NodeJS.Timeout | null>,
pollingSessionIdRef: React.MutableRefObject<string | null>
) => {
if (pollingTimeoutRef.current) {
clearTimeout(pollingTimeoutRef.current);
pollingTimeoutRef.current = null;
}
pollingSessionIdRef.current = null;
};
@@ -0,0 +1,134 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
import React from 'react';
/**
* Toggles dropdown open status
*/
export const toggleDropdown = (
nodeId: string,
paramName: string,
event: React.MouseEvent,
openDropdowns: {[key: string]: boolean | {isOpen: boolean, x: number, y: number}},
setOpenDropdowns: React.Dispatch<React.SetStateAction<{[key: string]: boolean | {isOpen: boolean, x: number, y: number}}>>
) => {
event.preventDefault();
event.stopPropagation();
const dropdownKey = `${nodeId}_${paramName}`;
setOpenDropdowns(prev => {
const isCurrentlyOpen = prev[dropdownKey] &&
typeof prev[dropdownKey] === 'object' ?
(prev[dropdownKey] as {isOpen: boolean}).isOpen :
Boolean(prev[dropdownKey]);
if (isCurrentlyOpen) {
return {
...prev,
[dropdownKey]: false
};
} else {
// Determine dropdown position
let x = 0;
let y = 0;
// Try to get position from event target
if (event.currentTarget) {
const rect = event.currentTarget.getBoundingClientRect();
x = rect.left;
y = rect.bottom;
} else {
// If unable to get element position, use mouse click position
x = event.clientX;
y = event.clientY + 20; // Display 20px below mouse position
}
// Ensure dropdown doesn't go out of window bounds
const windowWidth = window.innerWidth;
const dropdownWidth = 250; // Estimated dropdown width
if (x + dropdownWidth > windowWidth) {
x = windowWidth - dropdownWidth - 10; // Ensure distance from right edge at least 10px
}
if (x < 0) x = 10; // Ensure distance from left edge at least 10px
return {
...prev,
[dropdownKey]: {
isOpen: true,
x: x,
y: y
}
};
}
});
};
/**
* Updates parameter test values
*/
export const updateParamTestValues = (
nodeId: string,
paramName: string,
values: any[],
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}},
setParamTestValues: React.Dispatch<React.SetStateAction<{[nodeId: string]: {[paramName: string]: any[]}}>>
) => {
setParamTestValues(prev => {
const updatedValues = { ...prev };
// Ensure nodeID exists
if (!updatedValues[nodeId]) {
updatedValues[nodeId] = {};
}
// Update specific node and parameter values
updatedValues[nodeId][paramName] = values;
return updatedValues;
});
};
/**
* Handles selecting specific test values
*/
export const handleTestValueSelect = (
nodeId: string,
paramName: string,
value: any,
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}},
setParamTestValues: React.Dispatch<React.SetStateAction<{[nodeId: string]: {[paramName: string]: any[]}}>>,
event?: React.MouseEvent
) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
setParamTestValues(prev => {
const updatedValues = { ...prev };
// Ensure nodeID exists
if (!updatedValues[nodeId]) {
updatedValues[nodeId] = {};
}
// Ensure parameter name exists
if (!updatedValues[nodeId][paramName]) {
updatedValues[nodeId][paramName] = [];
}
const currentValues = updatedValues[nodeId][paramName];
if (currentValues.includes(value)) {
updatedValues[nodeId][paramName] = currentValues.filter(v => v !== value);
} else {
updatedValues[nodeId][paramName] = [...currentValues, value];
}
return updatedValues;
});
};
@@ -0,0 +1,42 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
// Add constant for localStorage key - used to persist interface state between sessions
export const PARAM_DEBUG_STORAGE_KEY = 'parameter_debug_state';
/**
* Saves parameter debug state to localStorage
*/
export const saveStateToLocalStorage = (state: any) => {
try {
localStorage.setItem(PARAM_DEBUG_STORAGE_KEY, JSON.stringify(state));
} catch (error) {
console.error('Error saving parameter debug state:', error);
}
};
/**
* Loads parameter debug state from localStorage
*/
export const loadStateFromLocalStorage = () => {
try {
const savedState = localStorage.getItem(PARAM_DEBUG_STORAGE_KEY);
if (savedState) {
return JSON.parse(savedState);
}
} catch (error) {
console.error('Error loading parameter debug state:', error);
}
return null;
};
/**
* Clears parameter debug state from localStorage
*/
export const clearStateFromLocalStorage = () => {
try {
localStorage.removeItem(PARAM_DEBUG_STORAGE_KEY);
} catch (error) {
console.error('Error clearing parameter debug state:', error);
}
};
@@ -0,0 +1,75 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
import React from 'react';
/**
* 打开图像模态框
*/
export const openImageModal = (
imageUrl: string,
params: { [key: string]: any },
selectedNodes: any[],
setModalImageUrl: React.Dispatch<React.SetStateAction<string>>,
setModalImageParams: React.Dispatch<React.SetStateAction<{ [key: string]: any } | null>>,
setModalVisible: React.Dispatch<React.SetStateAction<boolean>>,
event: React.MouseEvent
) => {
event.preventDefault();
event.stopPropagation();
// 添加节点名称信息
const enhancedParams = { ...params };
// 如果有nodeParams,为每个节点添加节点名称
if (params.nodeParams) {
// 创建nodeNames映射
const nodeNames: {[nodeId: string]: string} = {};
Object.keys(params.nodeParams).forEach(nodeId => {
// 尝试从selectedNodes中找到对应节点的名称
const node = selectedNodes.find(n => n.id.toString() === nodeId);
nodeNames[nodeId] = node ? (node.title || `Node ${nodeId}`) : `Node ${nodeId}`;
});
// 添加节点名称信息到参数中
enhancedParams.nodeNames = nodeNames;
}
// 如果没有nodeParams但有其他参数,创建一个简单的nodeParams结构
else if (Object.keys(params).length > 0) {
// 创建一个模拟的nodeParams结构
const simpleNode: {[key: string]: any} = {};
Object.entries(params).forEach(([key, value]) => {
// 只处理非对象类型的值
if (typeof value !== 'object' || value === null) {
simpleNode[key] = value;
}
});
if (Object.keys(simpleNode).length > 0) {
enhancedParams.nodeParams = {
default: simpleNode
};
enhancedParams.nodeNames = {
default: params.node_name || params.nodeName || "参数"
};
}
}
setModalImageUrl(imageUrl);
setModalImageParams(enhancedParams);
setModalVisible(true);
};
/**
* 关闭图像模态框
*/
export const closeImageModal = (
setModalVisible: React.Dispatch<React.SetStateAction<boolean>>,
event?: React.MouseEvent
) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
setModalVisible(false);
};
@@ -0,0 +1,172 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
import React from 'react';
/**
* 处理页面跳转到下一步
*/
export const handleNext = (
currentScreen: number,
setCurrentScreen: React.Dispatch<React.SetStateAction<number>>,
selectedParams: {[key: string]: boolean},
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}},
setParamTestValues: React.Dispatch<React.SetStateAction<{[nodeId: string]: {[paramName: string]: any[]}}>>,
textInputs: {[nodeId_paramName: string]: string[]},
selectedNodes: any[],
setTotalCount: React.Dispatch<React.SetStateAction<number>>,
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>,
generateParameterCombinations: (paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}}) => any[],
event?: React.MouseEvent
) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
// When moving from screen 0 to screen 1, clean up paramTestValues for unselected parameters
if (currentScreen === 0) {
// Make a copy of the current paramTestValues
const updatedParamTestValues = { ...paramTestValues };
// For each node
Object.keys(updatedParamTestValues).forEach(nodeId => {
const nodeParams = updatedParamTestValues[nodeId];
// For each parameter in this node
Object.keys(nodeParams).forEach(paramName => {
// If this parameter is no longer selected, remove it
if (!selectedParams[paramName]) {
delete nodeParams[paramName];
}
});
});
// Update the state with cleaned up values
setParamTestValues(updatedParamTestValues);
}
// When moving to the confirmation screen, ensure all text values are properly synchronized
if (currentScreen === 1) {
// Ensure textInputs are synced to paramTestValues for all selected nodes
const updatedParamTestValues = { ...paramTestValues };
selectedNodes.forEach(node => {
const nodeId = node.id.toString();
const widgets = node.widgets || {};
Object.values(widgets).forEach((widget: any) => {
const paramName = widget.name;
// Only process selected text parameters
if (selectedParams[paramName] && (widget.type === "customtext" || widget.type.toLowerCase().includes("text"))) {
const inputKey = `${nodeId}_${paramName}`;
const currentTexts = textInputs[inputKey] || [''];
// Update paramTestValues with the current text values
updatedParamTestValues[nodeId] = updatedParamTestValues[nodeId] || {};
updatedParamTestValues[nodeId][paramName] = currentTexts;
}
});
});
// Update the state with synchronized values
setParamTestValues(updatedParamTestValues);
// Update totalCount with parameter combinations
const combinations = generateParameterCombinations(updatedParamTestValues);
setTotalCount(combinations.length);
}
// Clear error message
setErrorMessage(null);
setCurrentScreen(prev => Math.min(prev + 1, 2));
};
/**
* 处理页面跳转到上一步
*/
export const handlePrevious = (
currentScreen: number,
setCurrentScreen: React.Dispatch<React.SetStateAction<number>>,
selectedNodes: any[],
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}},
setParamTestValues: React.Dispatch<React.SetStateAction<{[nodeId: string]: {[paramName: string]: any[]}}>>,
textInputs: {[nodeId_paramName: string]: string[]},
isCompleted: boolean,
setIsCompleted: React.Dispatch<React.SetStateAction<boolean>>,
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>,
event?: React.MouseEvent
) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
// Clear error message
setErrorMessage(null);
// Reset completed state when going back
if (isCompleted) {
setIsCompleted(false);
}
// If going back from screen 1 to 0, clean up any paramTestValues for nodes that are no longer selected
if (currentScreen === 1) {
const selectedNodeIds = selectedNodes.map(node => node.id.toString());
const currentNodeIds = Object.keys(paramTestValues);
// Remove test values for any nodes that are no longer selected
const updatedParamTestValues = { ...paramTestValues };
currentNodeIds.forEach(nodeId => {
if (!selectedNodeIds.includes(nodeId)) {
delete updatedParamTestValues[nodeId];
}
});
setParamTestValues(updatedParamTestValues);
}
// If returning from the result gallery or confirmation screen, ensure text values are maintained
if (currentScreen === 2 || isCompleted) {
// Ensure paramTestValues are correctly synchronized with textInputs
const updatedParamTestValues = { ...paramTestValues };
// For each text input value, ensure it's properly synced to paramTestValues
Object.entries(textInputs).forEach(([key, texts]) => {
if (!texts || texts.length === 0) return;
const [nodeId, paramName] = key.split('_');
// Skip if node doesn't exist in selected nodes
if (!selectedNodes.some(node => node.id.toString() === nodeId)) return;
// Ensure the node and parameter exist in paramTestValues
updatedParamTestValues[nodeId] = updatedParamTestValues[nodeId] || {};
// Only update if the values are different to avoid unnecessary state updates
if (JSON.stringify(updatedParamTestValues[nodeId][paramName]) !== JSON.stringify(texts)) {
updatedParamTestValues[nodeId][paramName] = texts;
}
});
setParamTestValues(updatedParamTestValues);
}
setCurrentScreen(prev => Math.max(prev - 1, 0));
};
/**
* 处理页码变化
*/
export const handlePageChange = (
newPage: number,
imagesPerPage: number,
generatedImages: any[],
setCurrentPage: React.Dispatch<React.SetStateAction<number>>,
event?: React.MouseEvent
) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
setCurrentPage(Math.max(1, Math.min(newPage, Math.ceil(generatedImages.length / imagesPerPage))));
};
@@ -0,0 +1,90 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
import React from 'react';
/**
* Process search input
*/
export const handleSearch = (
nodeId: string,
paramName: string,
term: string,
searchTerms: {[key: string]: string},
setSearchTerms: React.Dispatch<React.SetStateAction<{[key: string]: string}>>
) => {
const searchKey = `${nodeId}_${paramName}`;
setSearchTerms(prev => ({
...prev,
[searchKey]: term
}));
};
/**
* Handle select all functionality
*/
export const handleSelectAll = (
nodeId: string,
paramName: string,
values: any[],
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}},
updateParamTestValues: (nodeId: string, paramName: string, values: any[]) => void
) => {
// Ensure nodeID exists
if (!paramTestValues[nodeId]) {
updateParamTestValues(nodeId, paramName, values);
return;
}
// Get current values
const currentValues = paramTestValues[nodeId][paramName] || [];
// If all values are selected, cancel selecting all, otherwise select all
if (values.length === currentValues.length) {
updateParamTestValues(nodeId, paramName, []);
} else {
updateParamTestValues(nodeId, paramName, [...values]);
}
};
/**
* Handle parameter selection
*/
export const handleParamSelect = (
param: string,
selectedParams: {[key: string]: boolean},
setSelectedParams: React.Dispatch<React.SetStateAction<{[key: string]: boolean}>>,
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}},
setParamTestValues: React.Dispatch<React.SetStateAction<{[nodeId: string]: {[paramName: string]: any[]}}>>,
event?: React.MouseEvent
) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
const isCurrentlySelected = selectedParams[param];
// Update the selected params state
setSelectedParams(prev => ({
...prev,
[param]: !isCurrentlySelected
}));
// If we're deselecting a parameter, remove its values from paramTestValues
if (isCurrentlySelected) {
// Make a copy of the current paramTestValues
const updatedParamTestValues = { ...paramTestValues };
// For each node
Object.keys(updatedParamTestValues).forEach(nodeId => {
// If this parameter exists in this node, remove it
if (updatedParamTestValues[nodeId][param]) {
delete updatedParamTestValues[nodeId][param];
}
});
// Update the state with cleaned up values
setParamTestValues(updatedParamTestValues);
}
};
@@ -0,0 +1,251 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
import { generateUUID } from '../../../utils/uuid';
import { clearStateFromLocalStorage } from './localStorageUtils';
import { WorkflowChatAPI } from '../../../apis/workflowChatApi';
/**
* 重置所有状态变量
*/
export const resetAllStates = (
setCurrentScreen: React.Dispatch<React.SetStateAction<number>>,
setSelectedParams: React.Dispatch<React.SetStateAction<{[key: string]: boolean}>>,
setTask_id: React.Dispatch<React.SetStateAction<string>>,
cleanupPolling: () => void,
setIsProcessing: React.Dispatch<React.SetStateAction<boolean>>,
setIsCompleted: React.Dispatch<React.SetStateAction<boolean>>,
setCompletedCount: React.Dispatch<React.SetStateAction<number>>,
setTotalCount: React.Dispatch<React.SetStateAction<number>>,
setSelectedImageIndex: React.Dispatch<React.SetStateAction<number | null>>,
setGeneratedImages: React.Dispatch<React.SetStateAction<any[]>>,
setParamTestValues: React.Dispatch<React.SetStateAction<{[nodeId: string]: {[paramName: string]: any[]}}>>,
setOpenDropdowns: React.Dispatch<React.SetStateAction<{[key: string]: boolean | {isOpen: boolean, x: number, y: number}}>>,
setSearchTerms: React.Dispatch<React.SetStateAction<{[key: string]: string}>>,
setInputValues: React.Dispatch<React.SetStateAction<{[nodeId_paramName: string]: {min?: string, max?: string, step?: string}}>>,
setCurrentPage: React.Dispatch<React.SetStateAction<number>>,
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>,
setNotificationVisible: React.Dispatch<React.SetStateAction<boolean>>,
setModalVisible: React.Dispatch<React.SetStateAction<boolean>>,
setModalImageUrl: React.Dispatch<React.SetStateAction<string>>,
setModalImageParams: React.Dispatch<React.SetStateAction<{ [key: string]: any } | null>>,
setTextInputs: React.Dispatch<React.SetStateAction<{[nodeId_paramName: string]: string[]}>>,
setAiWritingModalVisible: React.Dispatch<React.SetStateAction<boolean>>,
setAiWritingModalText: React.Dispatch<React.SetStateAction<string>>,
setAiGeneratedTexts: React.Dispatch<React.SetStateAction<string[]>>,
setAiWritingLoading: React.Dispatch<React.SetStateAction<boolean>>,
setAiWritingNodeId: React.Dispatch<React.SetStateAction<string>>,
setAiWritingParamName: React.Dispatch<React.SetStateAction<string>>,
setAiWritingError: React.Dispatch<React.SetStateAction<string | null>>,
setAiSelectedTexts: React.Dispatch<React.SetStateAction<{[key: string]: boolean}>>,
clearStorage: boolean = false
) => {
setCurrentScreen(0);
setSelectedParams({
Steps: true,
CFG: true,
sampler_name: true,
threshold: false,
prompt: false
});
// Reset task_id
setTask_id(generateUUID());
cleanupPolling();
setIsProcessing(false);
setIsCompleted(false);
setCompletedCount(0);
setTotalCount(12);
setSelectedImageIndex(null);
setGeneratedImages(Array(12).fill(null).map((_, i) => ({
url: `https://source.unsplash.com/random/300x300?sig=${Math.random()}`,
params: {
step: i % 3 === 0 ? 5 : i % 3 === 1 ? 10 : 15,
sampler_name: 'euler',
cfg: 1
}
})));
setParamTestValues({});
setOpenDropdowns({});
setSearchTerms({});
setInputValues({});
setCurrentPage(1);
setErrorMessage(null);
setNotificationVisible(false);
setModalVisible(false);
setModalImageUrl('');
setModalImageParams(null);
// Reset text inputs
setTextInputs({});
setAiWritingModalVisible(false);
setAiWritingModalText('');
setAiGeneratedTexts([]);
setAiWritingLoading(false);
setAiWritingNodeId('');
setAiWritingParamName('');
setAiWritingError(null);
setAiSelectedTexts({});
// Clear localStorage if requested (only when explicitly closing the interface)
if (clearStorage) {
clearStateFromLocalStorage();
}
};
/**
* 处理选择图像
*/
export const handleSelectImage = (
index: number,
setSelectedImageIndex: React.Dispatch<React.SetStateAction<number | null>>,
event?: React.MouseEvent
) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
setSelectedImageIndex(index);
};
/**
* 处理应用选中的图像配置
*/
export const handleApplySelected = async (
selectedImageIndex: number | null,
generatedImages: any[],
app: any,
task_id: string,
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}},
setNotificationVisible: React.Dispatch<React.SetStateAction<boolean>>,
event?: React.MouseEvent
) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (selectedImageIndex !== null) {
console.log(`Applied image ${selectedImageIndex + 1} with parameters:`, generatedImages[selectedImageIndex].params);
// Send tracking event
let count_temp = 1;
// Calculate total parameter combinations
if (paramTestValues) {
for (const nodeId in paramTestValues) {
if (paramTestValues[nodeId]) {
for (const paramName in paramTestValues[nodeId]) {
const paramOptions = paramTestValues[nodeId][paramName];
if (paramOptions) {
let optionsCount = 1;
if (Array.isArray(paramOptions)) {
optionsCount = paramOptions.length > 0 ? paramOptions.length : 1;
} else if (typeof paramOptions === 'object' && paramOptions !== null) {
// Handle both numeric and string keys in objects
optionsCount = Object.keys(paramOptions).length > 0 ? Object.keys(paramOptions).length : 1;
}
count_temp = count_temp * optionsCount;
}
}
}
}
}
WorkflowChatAPI.trackEvent({
event_type: 'parameter_debug_apply',
message_type: 'parameter_debug',
message_id: task_id,
data: {
workflow: (await app.graphToPrompt()).output,
selected_params: generatedImages[selectedImageIndex].params,
all_params: paramTestValues,
count: count_temp
}
});
// Apply selected image parameters to canvas
const selectedParams = generatedImages[selectedImageIndex].params;
// Use structured parameter format
if (selectedParams.nodeParams) {
// Iterate over all node parameters
Object.entries(selectedParams.nodeParams).forEach(([nodeId, nodeParams]) => {
// Get node
const node = app.graph._nodes_by_id[nodeId];
if (!node || !node.widgets) return;
// Iterate over node parameters
Object.entries(nodeParams as Record<string, any>).forEach(([paramName, value]) => {
// Find corresponding widget in node widgets
for (const widget of node.widgets) {
if (widget.name === paramName) {
// Set widget value
widget.value = value;
break;
}
}
});
});
// Mark canvas as dirty, trigger re-render
app.graph.setDirtyCanvas(false, true);
// Show notification message
setNotificationVisible(true);
// Hide notification after 3 seconds
setTimeout(() => {
setNotificationVisible(false);
}, 3000);
}
}
};
/**
* 处理关闭
*/
export const handleClose = (
currentScreen: number,
selectedNode: any[],
nodeIndex: number | undefined,
dispatch: any,
onClose: (() => void) | undefined,
resetAllStates: (clearStorage: boolean) => void,
event?: React.MouseEvent
) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
// Screen 2: Close individual node card
if (currentScreen === 1 && nodeIndex !== undefined) {
// Remove the node at specified index
const newSelectedNodes = [...selectedNode];
const removedNode = newSelectedNodes[nodeIndex];
newSelectedNodes.splice(nodeIndex, 1);
// If no nodes left, return to original screen
if (newSelectedNodes.length === 0) {
if (onClose) {
// Clear screen state from context when closing
dispatch({ type: 'SET_SCREEN_STATE', payload: null });
// Clear all state variables and localStorage
resetAllStates(true); // Pass true to clear localStorage
// Call provided onClose to reset the interface
onClose();
}
} else {
// Update the context with the new selected nodes
dispatch({ type: 'SET_SELECTED_NODE', payload: newSelectedNodes });
}
return;
}
// Screen 3, 4, 5 or original: Close the entire interface
if (onClose) {
// Clear screen state from context when closing
dispatch({ type: 'SET_SCREEN_STATE', payload: null });
// Clear all state variables and localStorage
resetAllStates(true); // Pass true to clear localStorage
// Use the provided onClose callback
onClose();
}
};
@@ -0,0 +1,81 @@
// Copyright (C) 2025 AIDC-AI
// Licensed under the MIT License.
/**
* Updates text input values in the state
*/
export const handleTextInputChange = (
nodeId: string,
paramName: string,
index: number,
value: string,
textInputs: {[key: string]: string[]},
setTextInputs: React.Dispatch<React.SetStateAction<{[key: string]: string[]}>>
) => {
const textKey = `${nodeId}_${paramName}`;
setTextInputs(prev => {
const currentTexts = [...(prev[textKey] || [])];
currentTexts[index] = value;
return {
...prev,
[textKey]: currentTexts
};
});
// Also return the current texts with the new value directly
const currentTexts = [...(textInputs[textKey] || [])];
currentTexts[index] = value;
return currentTexts;
};
/**
* Adds a new text input
*/
export const handleAddTextInput = (
nodeId: string,
paramName: string,
textInputs: {[key: string]: string[]},
setTextInputs: React.Dispatch<React.SetStateAction<{[key: string]: string[]}>>
) => {
const textKey = `${nodeId}_${paramName}`;
setTextInputs(prev => {
const currentTexts = [...(prev[textKey] || [])];
currentTexts.push('');
return {
...prev,
[textKey]: currentTexts
};
});
// Return the updated texts
return [...(textInputs[textKey] || []), ''];
};
/**
* Removes a text input
*/
export const handleRemoveTextInput = (
nodeId: string,
paramName: string,
index: number,
textInputs: {[key: string]: string[]},
setTextInputs: React.Dispatch<React.SetStateAction<{[key: string]: string[]}>>
) => {
const textKey = `${nodeId}_${paramName}`;
setTextInputs(prev => {
const currentTexts = [...(prev[textKey] || [])];
currentTexts.splice(index, 1);
return {
...prev,
[textKey]: currentTexts
};
});
// Return the updated texts
const updatedTexts = [...(textInputs[textKey] || [])];
updatedTexts.splice(index, 1);
return updatedTexts;
};
+53 -19
View File
@@ -252,6 +252,9 @@
.top-2 {
top: 0.5rem;
}
.top-3 {
top: 0.75rem;
}
.top-4 {
top: 1rem;
}
@@ -264,6 +267,9 @@
.z-\[9999\] {
z-index: 9999;
}
.m-4 {
margin: 1rem;
}
.\!my-0\.5 {
margin-top: 0.125rem !important;
margin-bottom: 0.125rem !important;
@@ -411,9 +417,6 @@
.h-full {
height: 100%;
}
.max-h-16 {
max-height: 4rem;
}
.max-h-60 {
max-height: 15rem;
}
@@ -429,8 +432,11 @@
.max-h-\[90vh\] {
max-height: 90vh;
}
.max-h-\[calc\(90vh-4rem\)\] {
max-height: calc(90vh - 4rem);
.max-h-\[calc\(90vh-2rem\)\] {
max-height: calc(90vh - 2rem);
}
.max-h-full {
max-height: 100%;
}
.min-h-\[80px\] {
min-height: 80px;
@@ -441,9 +447,6 @@
.w-1\/2 {
width: 50%;
}
.w-10 {
width: 2.5rem;
}
.w-12 {
width: 3rem;
}
@@ -474,6 +477,9 @@
.w-64 {
width: 16rem;
}
.w-7 {
width: 1.75rem;
}
.w-8 {
width: 2rem;
}
@@ -483,18 +489,24 @@
.w-\[100\%\] {
width: 100%;
}
.w-\[35\%\] {
width: 35%;
}
.w-\[480px\] {
width: 480px;
}
.w-\[500px\] {
width: 500px;
}
.w-\[65\%\] {
width: 65%;
}
.w-\[90\%\] {
width: 90%;
}
.w-\[calc\(50\%-0\.25rem\)\] {
width: calc(50% - 0.25rem);
}
.w-auto {
width: auto;
}
.w-full {
width: 100%;
}
@@ -504,8 +516,8 @@
.max-w-2xl {
max-width: 42rem;
}
.max-w-4xl {
max-width: 56rem;
.max-w-6xl {
max-width: 72rem;
}
.max-w-full {
max-width: 100%;
@@ -588,6 +600,9 @@
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flex-row {
flex-direction: row;
}
.flex-col {
flex-direction: column;
}
@@ -651,9 +666,6 @@
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;
}
@@ -684,6 +696,9 @@
.break-words {
overflow-wrap: break-word;
}
.break-all {
word-break: break-all;
}
.rounded {
border-radius: 0.25rem;
}
@@ -723,6 +738,9 @@
.border-l-4 {
border-left-width: 4px;
}
.border-r {
border-right-width: 1px;
}
.border-t {
border-top-width: 1px;
}
@@ -1027,6 +1045,9 @@
.pb-3 {
padding-bottom: 0.75rem;
}
.pl-4 {
padding-left: 1rem;
}
.pr-12 {
padding-right: 3rem;
}
@@ -1066,9 +1087,6 @@
.text-\[14px\] {
font-size: 14px;
}
.text-\[16px\] {
font-size: 16px;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
@@ -1098,6 +1116,9 @@
.font-semibold {
font-weight: 600;
}
.italic {
font-style: italic;
}
.leading-relaxed {
line-height: 1.625;
}
@@ -1120,6 +1141,10 @@
--tw-text-opacity: 1;
color: rgb(146 64 14 / var(--tw-text-opacity, 1));
}
.text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity, 1));
}
.text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
@@ -1177,6 +1202,10 @@
--tw-text-opacity: 1;
color: rgb(79 70 229 / var(--tw-text-opacity, 1));
}
.text-pink-600 {
--tw-text-opacity: 1;
color: rgb(219 39 119 / var(--tw-text-opacity, 1));
}
.text-pink-700 {
--tw-text-opacity: 1;
color: rgb(190 24 93 / var(--tw-text-opacity, 1));
@@ -1642,6 +1671,11 @@
color: rgb(74 222 128 / var(--tw-text-opacity, 1));
}
.dark\:text-pink-400 {
--tw-text-opacity: 1;
color: rgb(244 114 182 / var(--tw-text-opacity, 1));
}
.dark\:text-red-400 {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity, 1));
+4 -4
View File
@@ -1,8 +1,8 @@
/*
* @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-04-22 17:48:28
* @LastEditors: ai-business-hql qingli.hql@alibaba-inc.com
* @LastEditTime: 2025-05-14 20:48:24
* @FilePath: /comfyui_copilot/ui/src/workflowChat/workflowChat.tsx
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@@ -103,7 +103,7 @@ const ParameterDebugTab = () => {
const selectedNodes = selectedNode ? selectedNode : [];
const ParameterDebugInterface = React.lazy(() =>
import("../components/debug/ParameterDebugInterfaceNew").then(module => ({
import("../components/debug/ParameterDebugInterfaceV2").then(module => ({
default: module.ParameterDebugInterface
}))
);
@@ -141,7 +141,7 @@ const TabButton = ({
}) => (
<button
onClick={onClick}
className={`px-4 py-2 font-medium text-sm transition-colors duration-200 ${
className={`px-4 py-2 font-medium text-xs 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"