feat: genlab optimize v2
This commit is contained in:
@@ -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
-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-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 _};
|
||||
|
||||
+5
-5
File diff suppressed because one or more lines are too long
Generated
+4
-3
@@ -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",
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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';
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user