Files
AIDC-AI-ComfyUI-Copilot/ui/src/components/debug/screens/ConfigureParameterScreen.tsx
T

690 lines
37 KiB
TypeScript

import React, { useEffect } from 'react';
interface ConfigureParameterScreenProps {
selectedNodes: any[];
paramTestValues: {[nodeId: string]: {[paramName: string]: any[]}};
selectedParams: {[key: string]: boolean};
updateParamTestValues: (nodeId: string, paramName: string, values: any[]) => void;
toggleDropdown: (nodeId: string, paramName: string, event: React.MouseEvent) => void;
openDropdowns: {[key: string]: boolean | {isOpen: boolean, x: number, y: number}};
handleTestValueSelect: (nodeId: string, paramName: string, value: any, event?: React.MouseEvent) => void;
searchTerms: {[key: string]: string};
handleSearch: (nodeId: string, paramName: string, term: string) => void;
handleSelectAll: (nodeId: string, paramName: string, values: any[]) => void;
handleNext: (event?: React.MouseEvent) => void;
handlePrevious: (event?: React.MouseEvent) => void;
handleClose: (event?: React.MouseEvent, nodeIndex?: number) => void;
inputValues: {[nodeId_paramName: string]: {min?: string, max?: string, step?: string}};
setInputValues: React.Dispatch<React.SetStateAction<{[nodeId_paramName: string]: {min?: string, max?: string, step?: string}}>>;
textInputs: {[nodeId_paramName: string]: string[]};
handleTextInputChange: (nodeId: string, paramName: string, index: number, value: string) => void;
handleAddTextInput: (nodeId: string, paramName: string) => void;
handleRemoveTextInput: (nodeId: string, paramName: string, index: number) => void;
openAiWritingModal: (nodeId: string, paramName: string) => void;
}
export const ConfigureParameterScreen: React.FC<ConfigureParameterScreenProps> = ({
selectedNodes,
paramTestValues,
selectedParams,
updateParamTestValues,
toggleDropdown,
openDropdowns,
handleTestValueSelect,
searchTerms,
handleSearch,
handleSelectAll,
handleNext,
handlePrevious,
handleClose,
inputValues,
setInputValues,
textInputs,
handleTextInputChange,
handleAddTextInput,
handleRemoveTextInput,
openAiWritingModal
}) => {
// Function to generate numeric test values
const generateNumericTestValues = (min: number, max: number, step: number, precision: number = 0) => {
// Ensure all parameters have valid values
min = isNaN(min) ? 0 : min;
max = isNaN(max) ? 100 : max;
step = isNaN(step) || step <= 0 ? 1 : step;
// Ensure max >= min
if (max < min) {
max = min;
}
// Calculate count based on the actual step
const count = Math.floor((max - min) / step) + 1;
if (count <= 1) return [min];
// Generate values based on the actual step
const values = [];
let current = min;
// Generate all values between min and max using step
while (current <= max) {
// Apply precision based on the parameter
const factor = Math.pow(10, precision);
const roundedValue = Math.round(current * factor) / factor;
values.push(roundedValue);
current += step;
}
return values;
};
// Initialize default values for numeric parameters when component mounts
useEffect(() => {
if (!selectedNodes || selectedNodes.length === 0) return;
// For each selected node
selectedNodes.forEach(node => {
const nodeId = node.id.toString();
const widgets = node.widgets || {};
const nodeWidgets = Object.values(widgets);
// For each widget in the node
nodeWidgets.forEach((widget: any) => {
const paramName = widget.name;
// Only process selected parameters
if (!selectedParams[paramName]) return;
// Check if we already have values for this parameter
const hasValues = paramTestValues[nodeId]?.[paramName]?.length > 0;
// If no values exist yet and it's a numeric parameter
if (!hasValues && widget.type === "number") {
const min = widget.options?.min || 0;
const max = widget.options?.max || 100;
const step = (widget.options?.step || 10) / 10;
const precision = widget.options?.precision || 0;
// Generate default values
const defaultValues = generateNumericTestValues(min, max, step, precision);
// Store these values in the state
if (defaultValues.length > 0) {
updateParamTestValues(nodeId, paramName, defaultValues);
// Also update the input values state
const inputKey = `${nodeId}_${paramName}`;
setInputValues(prev => ({
...prev,
[inputKey]: {
min: min.toString(),
max: max.toString(),
step: step.toString()
}
}));
}
}
// Handle text inputs - Always ensure paramTestValues are in sync with textInputs
if ((widget.type === "customtext" || widget.type.toLowerCase().includes("text"))) {
const inputKey = `${nodeId}_${paramName}`;
const currentTexts = textInputs[inputKey] || [''];
// Always update paramTestValues with current textInputs to ensure they're in sync
// This ensures text values are maintained throughout the workflow
if (JSON.stringify(currentTexts) !== JSON.stringify(paramTestValues[nodeId]?.[paramName] || [''])) {
updateParamTestValues(nodeId, paramName, currentTexts);
}
}
});
});
}, [selectedNodes, selectedParams, paramTestValues, textInputs, updateParamTestValues, setInputValues, generateNumericTestValues]);
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<div>
<h3 className="text-base font-medium text-gray-800">Parameter Options</h3>
<p className="text-xs text-gray-500">Configure the test values for each parameter</p>
</div>
<button
className="text-gray-400 hover:text-gray-600"
onClick={handleClose}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{selectedNodes.length > 0 ? (
selectedNodes.map((node, nodeIndex) => {
const widgets = node.widgets || {};
const nodeWidgets = Object.values(widgets);
const nodeId = node.id; // 获取节点ID
return (
<div key={nodeIndex} className="border rounded-md mb-4 overflow-hidden">
<div className="bg-gray-50 px-3 py-2 text-xs font-medium text-gray-700 border-b flex justify-between items-center">
<span>{node.type} (ID: {nodeId})</span>
{/* Add close button for individual node card */}
<button
className="text-gray-400 hover:text-gray-600"
onClick={(e) => handleClose(e, nodeIndex)}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-4 space-y-4">
{nodeWidgets.map((widget: any, widgetIndex: number) => {
const paramName = widget.name;
// Only show selected parameters
if (!selectedParams[paramName]) {
return null;
}
// 为每个输入字段创建唯一的键值
const inputKey = `${nodeId}_${paramName}`;
// Handle text parameter type
if (widget.type === "customtext" || widget.type.toLowerCase().includes("text")) {
const currentTexts = textInputs[inputKey] || [''];
return (
<div key={widgetIndex} className="border-b pb-3">
<div className="flex justify-between items-center mb-2">
<label className="text-xs font-medium text-gray-700">{paramName}:</label>
<button
className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs flex items-center hover:bg-blue-200 transition-colors"
onClick={() => openAiWritingModal(nodeId, paramName)}
>
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
AI Writing
</button>
</div>
<div className="space-y-2">
{currentTexts.map((text, textIndex) => (
<div key={textIndex} className="flex items-start">
<textarea
className="flex-1 border border-gray-300 rounded p-2 text-xs resize-y"
rows={2}
placeholder="Enter text..."
value={text}
onChange={(e) => handleTextInputChange(nodeId, paramName, textIndex, e.target.value)}
/>
<button
className="ml-2 text-red-500 hover:text-red-700"
onClick={() => handleRemoveTextInput(nodeId, paramName, textIndex)}
title="Remove"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
</div>
<button
className="mt-2 px-2 py-1 border border-dashed border-gray-300 rounded text-xs text-gray-600 hover:bg-gray-50 flex items-center"
onClick={() => handleAddTextInput(nodeId, paramName)}
>
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Add Text
</button>
</div>
);
}
if (widget.type === "number") {
const min = widget.options?.min || 0;
const max = widget.options?.max || 100;
const step = (widget.options?.step || 10) / 10;
const precision = widget.options?.precision || 0;
const defaultValues = generateNumericTestValues(min, max, step, precision);
// 获取当前输入状态,如果不存在则初始化
const currentInputs = inputValues[inputKey] || {
min: min.toString(),
max: max.toString(),
step: step.toString()
};
return (
<div key={widgetIndex} className="border-b pb-3">
<div className="flex justify-between items-center">
<label className="text-xs font-medium text-gray-700">{paramName}:</label>
<div className="w-4"></div>
</div>
<div className="flex space-x-2 items-center">
<label className="text-xs text-gray-600">Min</label>
<input
type="text"
className="w-12 h-6 border border-gray-300 rounded text-xs px-2"
value={currentInputs.min}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
const value = e.target.value;
// 更新输入状态
setInputValues(prev => ({
...prev,
[inputKey]: {
...prev[inputKey],
min: value
}
}));
// 只有当输入为空或有效时才更新参数值
if (value === '' || !isNaN(parseFloat(value))) {
const newMin = value === '' ? 0 : parseFloat(value);
const newMax = parseFloat(currentInputs.max || max.toString());
const newStep = parseFloat(currentInputs.step || step.toString());
const newValues = generateNumericTestValues(newMin, newMax, newStep, precision);
updateParamTestValues(nodeId, paramName, newValues);
}
}}
/>
<label className="text-xs text-gray-600">Max</label>
<input
type="text"
className="w-12 h-6 border border-gray-300 rounded text-xs px-2"
value={currentInputs.max}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
const value = e.target.value;
// 更新输入状态
setInputValues(prev => ({
...prev,
[inputKey]: {
...prev[inputKey],
max: value
}
}));
// 只有当输入为空或有效时才更新参数值
if (value === '' || !isNaN(parseFloat(value))) {
const newMin = parseFloat(currentInputs.min || min.toString());
const newMax = value === '' ? newMin : parseFloat(value);
const newStep = parseFloat(currentInputs.step || step.toString());
const newValues = generateNumericTestValues(newMin, newMax, newStep, precision);
updateParamTestValues(nodeId, paramName, newValues);
}
}}
/>
<label className="text-xs text-gray-600">Step</label>
<input
type="text"
className="w-12 h-6 border border-gray-300 rounded text-xs px-2"
value={currentInputs.step}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
const value = e.target.value;
// 更新输入状态
setInputValues(prev => ({
...prev,
[inputKey]: {
...prev[inputKey],
step: value
}
}));
// 只有当输入为空或有效时才更新参数值
if (value === '' || (!isNaN(parseFloat(value)) && parseFloat(value) > 0)) {
const newMin = parseFloat(currentInputs.min || min.toString());
const newMax = parseFloat(currentInputs.max || max.toString());
const newStep = value === '' ? 1 : parseFloat(value);
const newValues = generateNumericTestValues(newMin, newMax, newStep, precision);
updateParamTestValues(nodeId, paramName, newValues);
}
}}
/>
</div>
<div className="mt-1">
<label className="text-xs text-gray-600">Test values</label>
<div className="mt-1 flex flex-wrap gap-1">
{(() => {
const values = paramTestValues[nodeId]?.[paramName] || defaultValues;
// If 10 values or less, show all of them
if (values.length <= 10) {
return values.map((value, idx) => (
<div
key={idx}
className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-md flex items-center text-xs cursor-pointer hover:bg-blue-200"
onClick={(e) => {
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<span>{value}</span>
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
));
} else {
// Show first 5, ellipsis, and last 5
const firstFive = values.slice(0, 5);
const lastFive = values.slice(values.length - 5);
return (
<>
{firstFive.map((value, idx) => (
<div
key={`first-${idx}`}
className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-md flex items-center text-xs cursor-pointer hover:bg-blue-200"
onClick={(e) => {
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<span>{value}</span>
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
))}
{/* Ellipsis indicator */}
<div className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded-md flex items-center text-xs">
...
</div>
{lastFive.map((value, idx) => (
<div
key={`last-${idx}`}
className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-md flex items-center text-xs cursor-pointer hover:bg-blue-200"
onClick={(e) => {
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<span>{value}</span>
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
))}
</>
);
}
})()}
</div>
</div>
<div className="mt-1">
<p className="text-xs text-gray-500">Values are distributed evenly between Min and Max</p>
</div>
</div>
);
} else if (widget.type === "combo") {
const values = widget.options?.values || [];
const dropdownKey = `${nodeId}_${paramName}`;
return (
<div key={widgetIndex} className="border-b pb-3">
<div className="flex justify-between items-center">
<label className="text-xs font-medium text-gray-700">{paramName}</label>
<div className="relative dropdown-container">
<button
className="appearance-none border border-gray-300 rounded px-2 py-0.5 pr-6 text-xs flex items-center bg-white text-gray-700"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleDropdown(nodeId, paramName, e);
}}
data-dropdown={dropdownKey}
>
<span>
{(paramTestValues[nodeId]?.[paramName] || []).length > 0
? `${paramName}: ${(paramTestValues[nodeId]?.[paramName] || []).length} selected`
: `Select ${paramName}`}
</span>
<div className="ml-2">
<svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/>
</svg>
</div>
</button>
{openDropdowns[dropdownKey] && (
<div
className="absolute z-[9999] mt-1 w-64 bg-white border border-gray-300 rounded-md shadow-lg dropdown-menu"
style={{
position: 'fixed',
top: typeof openDropdowns[dropdownKey] === 'object' ?
`${(openDropdowns[dropdownKey] as {y: number}).y}px` : '0px',
left: typeof openDropdowns[dropdownKey] === 'object' ?
`${(openDropdowns[dropdownKey] as {x: number}).x}px` : '0px',
maxHeight: '300px',
overflowY: 'auto',
}}
>
{/* Search box */}
<div className="p-2 border-b sticky top-0 bg-white">
<input
type="text"
className="w-full px-2 py-0.5 text-xs border border-gray-300 rounded"
placeholder="Search..."
value={searchTerms[dropdownKey] || ''}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
handleSearch(nodeId, paramName, e.target.value);
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
autoFocus
/>
</div>
{/* Select all option */}
<div
className="px-3 py-1.5 bg-gray-50 border-b cursor-pointer text-xs flex items-center text-gray-700 hover:bg-gray-100"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const filteredValues = values.filter((value: string) =>
!searchTerms[dropdownKey] ||
value.toLowerCase().includes((searchTerms[dropdownKey] || '').toLowerCase())
);
handleSelectAll(nodeId, paramName, filteredValues);
}}
>
<div className={`w-4 h-4 mr-2 border rounded-sm flex items-center justify-center ${
values.length > 0 &&
(paramTestValues[nodeId]?.[paramName] || []).length === values.length
? 'bg-blue-500 border-blue-500'
: (paramTestValues[nodeId]?.[paramName] || []).length > 0
? 'bg-blue-500 border-blue-500 opacity-50'
: 'border-gray-300'
}`}>
{(paramTestValues[nodeId]?.[paramName] || []).length > 0 && (
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="font-medium">
{values.length > 0 &&
(paramTestValues[nodeId]?.[paramName] || []).length === values.length
? 'Deselect All'
: 'Select All'}
</span>
</div>
{/* Scrollable option list */}
<div className="max-h-60 overflow-y-auto">
{values
.filter((value: string) =>
!searchTerms[dropdownKey] ||
value.toLowerCase().includes((searchTerms[dropdownKey] || '').toLowerCase())
)
.map((value: string, i: number) => (
<div
key={i}
className="px-3 py-1.5 hover:bg-gray-100 cursor-pointer text-xs flex items-center text-gray-700"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<div className={`w-4 h-4 mr-2 border rounded-sm flex items-center justify-center ${(paramTestValues[nodeId]?.[paramName] || []).includes(value) ? 'bg-blue-500 border-blue-500' : 'border-gray-300'}`}>
{(paramTestValues[nodeId]?.[paramName] || []).includes(value) && (
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="whitespace-normal overflow-hidden text-ellipsis">{value}</span>
</div>
))}
{/* No search results message */}
{values.filter((value: string) =>
!searchTerms[dropdownKey] ||
value.toLowerCase().includes((searchTerms[dropdownKey] || '').toLowerCase())
).length === 0 && (
<div className="px-3 py-4 text-sm text-gray-500 text-center">
No matching options
</div>
)}
</div>
</div>
)}
</div>
</div>
<div className="mt-2">
<label className="text-xs text-gray-600">Test values</label>
<div className="mt-1 flex flex-wrap gap-1">
{(paramTestValues[nodeId]?.[paramName] || []).map((value: string, idx: number) => (
<div
key={idx}
className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-md flex items-center text-xs"
>
<span>{value}</span>
<svg
className="w-4 h-4 ml-1 cursor-pointer hover:text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
))}
</div>
</div>
</div>
);
} else {
return (
<div key={widgetIndex} className="border-b pb-3">
<div className="flex justify-between items-center">
<label className="text-xs font-medium text-gray-700">{paramName}</label>
</div>
<div className="mt-2">
<label className="text-xs text-gray-600">Test values</label>
<div className="mt-1 flex flex-wrap gap-1">
{(paramTestValues[nodeId]?.[paramName] || []).map((value: string, idx: number) => (
<div
key={idx}
className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-md flex items-center text-xs"
>
<span>{value}</span>
<svg
className="w-4 h-4 ml-1 cursor-pointer hover:text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleTestValueSelect(nodeId, paramName, value, e);
}}
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
))}
<button
className="px-2 py-0.5 border border-dashed border-blue-300 text-blue-600 rounded-md text-xs flex items-center hover:bg-blue-50"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const newValue = `${paramName}${(paramTestValues[nodeId]?.[paramName] || []).length + 1}`;
updateParamTestValues(nodeId, paramName, [...(paramTestValues[nodeId]?.[paramName] || []), newValue]);
}}
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<span>Add</span>
</button>
</div>
</div>
</div>
);
}
})}
</div>
</div>
);
})
) : (
<div className="border rounded-md mb-4 p-4 text-center text-gray-500">
No nodes selected. Please select a node in the workflow to configure parameters.
</div>
)}
<div className="mt-6 flex justify-between">
<button
onClick={(e) => handlePrevious(e)}
className="px-3 py-1.5 text-xs bg-gray-100 text-gray-800 rounded-md hover:bg-gray-200 transition-colors"
>
Previous
</button>
<button
onClick={(e) => handleNext(e)}
className="px-3 py-1.5 text-xs bg-pink-200 text-pink-700 rounded-md hover:bg-pink-300 transition-colors"
>
Next
</button>
</div>
</div>
);
};