Spaces:
Running
Running
| /** | |
| * TensorShapeInspector - Hiα»n thα» thΓ΄ng tin shape tensor trΓͺn edge | |
| * Shows a tooltip with tensor name, shape, and data type when hovering/clicking on graph edges. | |
| * Looks up shape info from parsedModel inputs, outputs, and graph.valueInfo. | |
| * Requirements: 21.1, 21.2, 21.3, 21.4, 21.5 | |
| */ | |
| class TensorShapeInspector { | |
| /** | |
| * @param {Object} [options] | |
| * @param {Object} [options.cy] - Cytoscape instance to attach events to | |
| * @param {Object} [options.parsedModel] - Parsed ONNX model data | |
| */ | |
| constructor(options = {}) { | |
| /** @type {Object|null} Cytoscape instance */ | |
| this._cy = options.cy || null; | |
| /** @type {Object|null} Parsed model data */ | |
| this._parsedModel = options.parsedModel || null; | |
| /** @type {HTMLElement|null} Tooltip element */ | |
| this._tooltip = null; | |
| /** @type {Map<string, Object>} Cached tensor info lookup */ | |
| this._tensorInfoMap = new Map(); | |
| this._createTooltip(); | |
| if (this._parsedModel) { | |
| this._buildTensorInfoMap(this._parsedModel); | |
| } | |
| if (this._cy) { | |
| this._bindCytoscapeEvents(this._cy); | |
| } | |
| } | |
| // βββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Attach to a Cytoscape instance for edge hover/click events. | |
| * @param {Object} cy - Cytoscape core instance | |
| */ | |
| attachToCytoscape(cy) { | |
| if (this._cy) { | |
| this._unbindCytoscapeEvents(this._cy); | |
| } | |
| this._cy = cy; | |
| if (cy) { | |
| this._bindCytoscapeEvents(cy); | |
| } | |
| } | |
| /** | |
| * Update the parsed model data used for tensor shape lookups. | |
| * @param {Object} parsedModel | |
| */ | |
| updateModel(parsedModel) { | |
| this._parsedModel = parsedModel; | |
| this._tensorInfoMap.clear(); | |
| if (parsedModel) { | |
| this._buildTensorInfoMap(parsedModel); | |
| } | |
| } | |
| /** | |
| * Look up tensor info by name. | |
| * @param {string} tensorName | |
| * @returns {{ name: string, shape: Array, dataType: string }|null} | |
| */ | |
| lookupTensor(tensorName) { | |
| if (!tensorName) return null; | |
| return this._tensorInfoMap.get(tensorName) || null; | |
| } | |
| /** | |
| * Destroy the inspector and clean up resources. | |
| */ | |
| destroy() { | |
| if (this._cy) { | |
| this._unbindCytoscapeEvents(this._cy); | |
| this._cy = null; | |
| } | |
| this._removeTooltip(); | |
| this._parsedModel = null; | |
| this._tensorInfoMap.clear(); | |
| } | |
| // βββ Private βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Build a lookup map from tensor name β { name, shape, dataType }. | |
| * Sources: parsedModel.inputs, parsedModel.outputs, parsedModel.graph.valueInfo | |
| * @param {Object} parsedModel | |
| * @private | |
| */ | |
| _buildTensorInfoMap(parsedModel) { | |
| this._tensorInfoMap.clear(); | |
| // 1. Inputs | |
| if (Array.isArray(parsedModel.inputs)) { | |
| for (const inp of parsedModel.inputs) { | |
| if (inp.name) { | |
| this._tensorInfoMap.set(inp.name, { | |
| name: inp.name, | |
| shape: inp.shape || [], | |
| dataType: inp.dataType || 'UNKNOWN' | |
| }); | |
| } | |
| } | |
| } | |
| // 2. Outputs | |
| if (Array.isArray(parsedModel.outputs)) { | |
| for (const out of parsedModel.outputs) { | |
| if (out.name) { | |
| this._tensorInfoMap.set(out.name, { | |
| name: out.name, | |
| shape: out.shape || [], | |
| dataType: out.dataType || 'UNKNOWN' | |
| }); | |
| } | |
| } | |
| } | |
| // 3. value_info (intermediate tensors) | |
| const valueInfo = parsedModel.graph && parsedModel.graph.valueInfo; | |
| if (valueInfo && typeof valueInfo === 'object') { | |
| const entries = valueInfo instanceof Map | |
| ? valueInfo.entries() | |
| : Object.entries(valueInfo); | |
| for (const [key, vi] of entries) { | |
| if (key && !this._tensorInfoMap.has(key)) { | |
| this._tensorInfoMap.set(key, { | |
| name: vi.name || key, | |
| shape: vi.shape || [], | |
| dataType: vi.dataType || 'UNKNOWN' | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Bind mouseover, mouseout, and tap events on edges. | |
| * @param {Object} cy | |
| * @private | |
| */ | |
| _bindCytoscapeEvents(cy) { | |
| this._onEdgeMouseOver = (evt) => { | |
| const edge = evt.target; | |
| const mouseEvent = evt.originalEvent; | |
| this._showEdgeTooltip(edge, mouseEvent); | |
| }; | |
| this._onEdgeMouseOut = () => { | |
| this._hideTooltip(); | |
| }; | |
| this._onEdgeTap = (evt) => { | |
| const edge = evt.target; | |
| const mouseEvent = evt.originalEvent || evt.position; | |
| this._showEdgeTooltip(edge, mouseEvent); | |
| }; | |
| cy.on('mouseover', 'edge', this._onEdgeMouseOver); | |
| cy.on('mouseout', 'edge', this._onEdgeMouseOut); | |
| cy.on('tap', 'edge', this._onEdgeTap); | |
| } | |
| /** | |
| * Unbind edge events from Cytoscape. | |
| * @param {Object} cy | |
| * @private | |
| */ | |
| _unbindCytoscapeEvents(cy) { | |
| if (this._onEdgeMouseOver) { | |
| cy.off('mouseover', 'edge', this._onEdgeMouseOver); | |
| } | |
| if (this._onEdgeMouseOut) { | |
| cy.off('mouseout', 'edge', this._onEdgeMouseOut); | |
| } | |
| if (this._onEdgeTap) { | |
| cy.off('tap', 'edge', this._onEdgeTap); | |
| } | |
| this._onEdgeMouseOver = null; | |
| this._onEdgeMouseOut = null; | |
| this._onEdgeTap = null; | |
| } | |
| /** | |
| * Show tooltip for an edge. | |
| * @param {Object} edge - Cytoscape edge element | |
| * @param {MouseEvent|Object} mouseEvent | |
| * @private | |
| */ | |
| _showEdgeTooltip(edge, mouseEvent) { | |
| if (!this._tooltip || !edge || edge.length === 0) return; | |
| const edgeData = edge.data(); | |
| const tensorName = edgeData.label || ''; | |
| const tensorInfo = this.lookupTensor(tensorName); | |
| const html = this._buildTooltipHTML(tensorName, tensorInfo); | |
| this._tooltip.innerHTML = html; | |
| this._tooltip.style.display = 'block'; | |
| this._positionTooltip(mouseEvent); | |
| } | |
| /** | |
| * Build tooltip HTML for a tensor. | |
| * @param {string} tensorName | |
| * @param {Object|null} tensorInfo | |
| * @returns {string} | |
| * @private | |
| */ | |
| _buildTooltipHTML(tensorName, tensorInfo) { | |
| const lines = []; | |
| // Tensor name | |
| const displayName = tensorName || 'Unnamed tensor'; | |
| lines.push('<strong>' + this._escapeHtml(displayName) + '</strong>'); | |
| if (tensorInfo) { | |
| // Shape | |
| const shapeStr = tensorInfo.shape && tensorInfo.shape.length > 0 | |
| ? '[' + tensorInfo.shape.join(', ') + ']' | |
| : 'unknown'; | |
| lines.push('<span>Shape: ' + this._escapeHtml(shapeStr) + '</span>'); | |
| // Data type | |
| lines.push('<span>Type: ' + this._escapeHtml(tensorInfo.dataType) + '</span>'); | |
| } else { | |
| lines.push('<span>Shape: unknown</span>'); | |
| } | |
| return lines.join('<br>'); | |
| } | |
| /** | |
| * Hide the tooltip. | |
| * @private | |
| */ | |
| _hideTooltip() { | |
| if (this._tooltip) { | |
| this._tooltip.style.display = 'none'; | |
| } | |
| } | |
| /** | |
| * Create the tooltip DOM element. | |
| * @private | |
| */ | |
| _createTooltip() { | |
| const tooltip = document.createElement('div'); | |
| tooltip.className = 'tensor-shape-tooltip'; | |
| tooltip.setAttribute('role', 'tooltip'); | |
| tooltip.style.cssText = [ | |
| 'position: fixed', | |
| 'z-index: 10000', | |
| 'background: rgba(30,30,30,0.95)', | |
| 'color: #fff', | |
| 'padding: 8px 12px', | |
| 'border-radius: 6px', | |
| 'font-size: 12px', | |
| 'max-width: 300px', | |
| 'pointer-events: none', | |
| 'display: none', | |
| 'box-shadow: 0 2px 8px rgba(0,0,0,0.4)', | |
| 'line-height: 1.5', | |
| 'border-left: 3px solid #17a2b8' | |
| ].join(';'); | |
| document.body.appendChild(tooltip); | |
| this._tooltip = tooltip; | |
| } | |
| /** | |
| * Remove tooltip from DOM. | |
| * @private | |
| */ | |
| _removeTooltip() { | |
| if (this._tooltip && this._tooltip.parentNode) { | |
| this._tooltip.parentNode.removeChild(this._tooltip); | |
| } | |
| this._tooltip = null; | |
| } | |
| /** | |
| * Position tooltip near the mouse cursor. | |
| * @param {MouseEvent|Object} mouseEvent | |
| * @private | |
| */ | |
| _positionTooltip(mouseEvent) { | |
| if (!this._tooltip || !mouseEvent) return; | |
| const offset = 12; | |
| let x, y; | |
| if (mouseEvent.clientX !== undefined) { | |
| x = mouseEvent.clientX + offset; | |
| y = mouseEvent.clientY + offset; | |
| } else if (mouseEvent.x !== undefined) { | |
| // Cytoscape position object fallback | |
| x = mouseEvent.x + offset; | |
| y = mouseEvent.y + offset; | |
| } else { | |
| return; | |
| } | |
| // Keep within viewport | |
| const rect = this._tooltip.getBoundingClientRect(); | |
| if (x + rect.width > window.innerWidth) { | |
| x = x - rect.width - offset * 2; | |
| } | |
| if (y + rect.height > window.innerHeight) { | |
| y = y - rect.height - offset * 2; | |
| } | |
| this._tooltip.style.left = x + 'px'; | |
| this._tooltip.style.top = y + 'px'; | |
| } | |
| /** | |
| * Escape HTML special characters. | |
| * @param {string} str | |
| * @returns {string} | |
| * @private | |
| */ | |
| _escapeHtml(str) { | |
| return String(str) | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"'); | |
| } | |
| } | |
| // Export as global for browser usage | |
| window.TensorShapeInspector = TensorShapeInspector; | |