/** * NodeDetailPanel - Displays detailed information for a selected graph node * Shows node name, opType, domain, attributes, and input/output tensors. * Listens for 'node:selected' events via EventBus and hides when no node is selected. * Requirements: 16.1, 16.2, 16.3, 16.4, 16.5 */ class NodeDetailPanel { /** * @param {string} containerId - ID of the container element */ constructor(containerId) { this._containerId = containerId; this._container = document.getElementById(containerId); this._currentNodeData = null; /** @type {Function|null} */ this._unsubscribeEvent = null; /** @type {Function|null} */ this._unsubscribeState = null; if (!this._container) { console.warn(`[NodeDetailPanel] Container #${containerId} not found`); } this._bindEvents(); this._renderGuidance(); } // ─── Private ────────────────────────────────────────────────────────────── /** * Subscribe to EventBus and StateManager for node selection changes. * @private */ _bindEvents() { // Listen for node:selected via EventBus if (typeof EventBus !== 'undefined' && typeof CONFIG !== 'undefined') { this._unsubscribeEvent = EventBus.on(CONFIG.EVENTS.NODE_SELECTED, (data) => { if (data && data.nodeData) { this._onNodeSelected(data.nodeData); } else if (data && data.nodeId) { // Minimal data — show what we have this._onNodeSelected({ id: data.nodeId }); } }); } // Listen for selectedNodeId becoming null (background click) if (typeof StateManager !== 'undefined') { this._unsubscribeState = StateManager.subscribe('selectedNodeId', (newId) => { if (!newId) { this._onNodeDeselected(); } }); } } /** * Handle node selection. * @param {Object} nodeData * @private */ _onNodeSelected(nodeData) { this._currentNodeData = nodeData; this._render(nodeData); } /** * Handle node deselection (background click). * @private */ _onNodeDeselected() { this._currentNodeData = null; this._renderGuidance(); } /** * Escape HTML special characters. * @param {string} str * @returns {string} * @private */ _escapeHtml(str) { const div = document.createElement('div'); div.appendChild(document.createTextNode(String(str))); return div.innerHTML; } /** * Format an attribute value for display. * @param {*} val * @returns {string} Escaped HTML string * @private */ _formatAttrValue(val) { if (val === null || val === undefined) { return 'null'; } if (Array.isArray(val)) { const preview = val.slice(0, 8).map((v) => String(v)).join(', '); const suffix = val.length > 8 ? `, … (+${val.length - 8})` : ''; return this._escapeHtml(`[${preview}${suffix}]`); } if (typeof val === 'object') { try { const json = JSON.stringify(val, null, 2); return `
${this._escapeHtml(json)}`;
} catch (_) {
return this._escapeHtml(String(val));
}
}
return this._escapeHtml(String(val));
}
/**
* Build the header section (name, opType, domain).
* @param {Object} nodeData
* @returns {string}
* @private
*/
_buildHeader(nodeData) {
const name = nodeData.name || nodeData.label || nodeData.id || 'Unnamed';
const opType = nodeData.opType || 'N/A';
const domain = nodeData.domain || 'ai.onnx';
return `
No attributes
No ${label.toLowerCase()}
`; } const listItems = items.map((name) => `Click a node in the graph to view its details