feat: genlab optimize v2
This commit is contained in:
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;
|
||||
};
|
||||
Reference in New Issue
Block a user