255 lines
7.8 KiB
JavaScript
255 lines
7.8 KiB
JavaScript
/**
|
|
* F7: Smart Parameter Injection - Fix Handler Module
|
|
*
|
|
* Detects, renders, and applies parameter fixes suggested by the AI chat.
|
|
* Provides one-click fix application with visual feedback and undo support.
|
|
*/
|
|
|
|
import { getComfyNodeById, markComfyGraphDirty } from "./comfyui_frontend_compat.js";
|
|
|
|
export class FixHandler {
|
|
constructor(app) {
|
|
this.app = app; // ComfyUI app instance
|
|
this.appliedFixes = []; // History for undo
|
|
}
|
|
|
|
/**
|
|
* Detect fix JSON in message content
|
|
* Returns: {fixes: [...]} or null
|
|
*/
|
|
detectFixes(content) {
|
|
const regex = /```json\s*(\{[^`]*?"fixes"[^`]*?\})\s*```/gs;
|
|
const match = regex.exec(content);
|
|
if (!match) return null;
|
|
|
|
try {
|
|
const fixData = JSON.parse(match[1]);
|
|
return this.validateFixes(fixData) ? fixData : null;
|
|
} catch (e) {
|
|
console.warn('[FixHandler] Invalid fix JSON:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate fix schema
|
|
* Required keys: node_id, widget, to
|
|
* Optional keys: from, reason
|
|
*/
|
|
validateFixes(fixData) {
|
|
if (!fixData.fixes || !Array.isArray(fixData.fixes)) {
|
|
return false;
|
|
}
|
|
|
|
return fixData.fixes.every(fix => {
|
|
// Check required fields
|
|
if (!fix.node_id || !fix.widget || fix.to === undefined) {
|
|
return false;
|
|
}
|
|
|
|
// Validate node_id is string or number
|
|
if (typeof fix.node_id !== 'string' && typeof fix.node_id !== 'number') {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render fix buttons in message DOM
|
|
*
|
|
* @param {HTMLElement} messageDiv - The chat message div
|
|
* @param {Object} fixData - Fix data with {fixes: [...]}
|
|
* @param {Object} i18n - Internationalization object
|
|
*/
|
|
renderFixButtons(messageDiv, fixData, i18n) {
|
|
const container = document.createElement('div');
|
|
container.className = 'doctor-fix-container';
|
|
|
|
fixData.fixes.forEach((fix, index) => {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'doctor-fix-btn';
|
|
|
|
// Format button text with i18n support
|
|
const fromText = fix.from !== undefined ? ` ${fix.from} →` : '';
|
|
const buttonText = i18n?.fix_apply_button
|
|
? `${i18n.fix_apply_button}: ${fix.widget}${fromText} ${fix.to}`
|
|
: `⚡ Apply: ${fix.widget}${fromText} ${fix.to}`;
|
|
|
|
btn.innerHTML = buttonText;
|
|
btn.title = fix.reason || (i18n?.fix_apply_tooltip || 'Apply parameter fix');
|
|
btn.dataset.fixIndex = index;
|
|
|
|
btn.onclick = () => this.applyFix(fix, btn, i18n);
|
|
container.appendChild(btn);
|
|
});
|
|
|
|
messageDiv.appendChild(container);
|
|
}
|
|
|
|
/**
|
|
* Apply fix to canvas widget
|
|
*
|
|
* @param {Object} fix - Fix data {node_id, widget, to, from?, reason?}
|
|
* @param {HTMLButtonElement} button - The button element
|
|
* @param {Object} i18n - Internationalization object
|
|
*/
|
|
async applyFix(fix, button, i18n) {
|
|
try {
|
|
button.disabled = true;
|
|
button.textContent = i18n?.fix_applying || 'Applying...';
|
|
|
|
// Find node
|
|
const nodeId = parseInt(fix.node_id);
|
|
const node = getComfyNodeById(nodeId, this.app);
|
|
|
|
if (!node) {
|
|
throw new Error(i18n?.fix_error_node_not_found || `Node ${nodeId} not found`);
|
|
}
|
|
|
|
// Find widget
|
|
const widget = node.widgets?.find(w => w.name === fix.widget);
|
|
if (!widget) {
|
|
throw new Error(
|
|
i18n?.fix_error_widget_not_found || `Widget "${fix.widget}" not found`
|
|
);
|
|
}
|
|
|
|
// Validate widget type (security whitelist)
|
|
const safeTypes = ['number', 'slider', 'combo', 'text', 'toggle', 'converted-widget'];
|
|
const widgetType = widget.type || 'number';
|
|
|
|
if (!safeTypes.includes(widgetType)) {
|
|
throw new Error(
|
|
i18n?.fix_error_unsafe_type || `Unsafe widget type: ${widgetType}`
|
|
);
|
|
}
|
|
|
|
// Type coercion based on widget type
|
|
let newValue = fix.to;
|
|
if (widgetType === 'number' || widgetType === 'slider') {
|
|
newValue = parseFloat(fix.to);
|
|
if (isNaN(newValue)) {
|
|
throw new Error(i18n?.fix_error_invalid_number || 'Invalid number value');
|
|
}
|
|
} else if (widgetType === 'toggle') {
|
|
newValue = Boolean(fix.to);
|
|
}
|
|
|
|
// Store for undo
|
|
this.appliedFixes.push({
|
|
node_id: nodeId,
|
|
widget: fix.widget,
|
|
oldValue: widget.value,
|
|
newValue: newValue,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
// Apply fix
|
|
widget.value = newValue;
|
|
|
|
// Trigger callback if exists
|
|
if (widget.callback) {
|
|
widget.callback(newValue);
|
|
}
|
|
|
|
// Redraw canvas
|
|
markComfyGraphDirty(this.app, false, true);
|
|
|
|
// Success state
|
|
button.className = 'doctor-fix-btn applied';
|
|
button.textContent = i18n?.fix_applied || '✓ Applied';
|
|
|
|
// Flash node highlight
|
|
this.flashNodeHighlight(nodeId);
|
|
|
|
// Log success
|
|
console.log(`[FixHandler] Applied fix: Node ${nodeId}, ${fix.widget} = ${newValue}`);
|
|
|
|
} catch (error) {
|
|
console.error('[FixHandler] Apply failed:', error);
|
|
button.className = 'doctor-fix-btn error';
|
|
button.textContent = `✗ ${error.message}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flash visual highlight on node
|
|
*
|
|
* @param {number} nodeId - Node ID to highlight
|
|
*/
|
|
flashNodeHighlight(nodeId) {
|
|
const node = getComfyNodeById(nodeId, this.app);
|
|
if (!node) return;
|
|
|
|
const originalColor = node.color;
|
|
const originalBgColor = node.bgcolor;
|
|
|
|
// Set highlight colors (blue theme)
|
|
node.color = "#2563eb";
|
|
node.bgcolor = "#dbeafe";
|
|
markComfyGraphDirty(this.app, true, true);
|
|
|
|
// Restore after animation
|
|
setTimeout(() => {
|
|
node.color = originalColor;
|
|
node.bgcolor = originalBgColor;
|
|
markComfyGraphDirty(this.app, true, true);
|
|
}, 800);
|
|
}
|
|
|
|
/**
|
|
* Undo last applied fix
|
|
* Can be called from browser console for debugging/manual undo
|
|
*
|
|
* @returns {boolean} - True if undo was successful
|
|
*/
|
|
undoLastFix() {
|
|
if (this.appliedFixes.length === 0) {
|
|
console.warn('[FixHandler] No fixes to undo');
|
|
return false;
|
|
}
|
|
|
|
const last = this.appliedFixes.pop();
|
|
const node = getComfyNodeById(last.node_id, this.app);
|
|
const widget = node?.widgets?.find(w => w.name === last.widget);
|
|
|
|
if (!widget) {
|
|
console.error('[FixHandler] Cannot undo: widget not found');
|
|
return false;
|
|
}
|
|
|
|
widget.value = last.oldValue;
|
|
if (widget.callback) {
|
|
widget.callback(last.oldValue);
|
|
}
|
|
|
|
markComfyGraphDirty(this.app, false, true);
|
|
console.log(`[FixHandler] Undone: Node ${last.node_id}, ${last.widget} = ${last.oldValue}`);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get fix application history
|
|
*
|
|
* @returns {Array} - Array of applied fixes
|
|
*/
|
|
getHistory() {
|
|
return [...this.appliedFixes];
|
|
}
|
|
|
|
/**
|
|
* Clear fix history
|
|
*/
|
|
clearHistory() {
|
|
this.appliedFixes = [];
|
|
}
|
|
}
|
|
|
|
// Export for global access (testing, console use)
|
|
if (typeof window !== 'undefined') {
|
|
window.FixHandler = FixHandler;
|
|
}
|