841 lines
28 KiB
JavaScript
841 lines
28 KiB
JavaScript
/**
|
|
* Object View - Generic Python object introspection viewer
|
|
* Displays metrics, spectral graphs, and serialized data for:
|
|
* - PyTorch/NumPy tensors
|
|
* - PIL Images
|
|
* - SafeTensors models
|
|
* - Generic Python objects
|
|
*/
|
|
|
|
import { BaseView, escapeHtml } from "./base_view.js";
|
|
|
|
class ObjectView extends BaseView {
|
|
static id = "object";
|
|
static displayName = "Object";
|
|
static priority = 5;
|
|
|
|
static OBJECT_MARKER = "$WAS_OBJECT$";
|
|
|
|
static detect(content) {
|
|
try {
|
|
let jsonContent = content;
|
|
if (content.startsWith(this.OBJECT_MARKER)) {
|
|
jsonContent = content.slice(this.OBJECT_MARKER.length);
|
|
}
|
|
const parsed = JSON.parse(jsonContent);
|
|
if (parsed.type === "object_viewer" && Array.isArray(parsed.objects)) {
|
|
return 150;
|
|
}
|
|
} catch {}
|
|
return 0;
|
|
}
|
|
|
|
static getContentMarker() {
|
|
return this.OBJECT_MARKER;
|
|
}
|
|
|
|
static render(content, theme) {
|
|
let data;
|
|
try {
|
|
let jsonContent = content;
|
|
if (content.startsWith(this.OBJECT_MARKER)) {
|
|
jsonContent = content.slice(this.OBJECT_MARKER.length);
|
|
}
|
|
data = JSON.parse(jsonContent);
|
|
} catch {
|
|
return `<pre>Invalid object data</pre>`;
|
|
}
|
|
|
|
const objects = data.objects || [];
|
|
if (objects.length === 0) {
|
|
return `<div class="object-viewer"><p>No objects to display</p></div>`;
|
|
}
|
|
|
|
// Include inline scripts for toggle functions and histogram drawing
|
|
let html = `
|
|
<script>
|
|
function toggleObjectSection(id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.classList.toggle('collapsed');
|
|
}
|
|
function toggleSection(id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.classList.toggle('collapsed');
|
|
}
|
|
function drawHistograms() {
|
|
var canvases = document.querySelectorAll('.histogram-canvas');
|
|
canvases.forEach(function(canvas) {
|
|
var ctx = canvas.getContext('2d');
|
|
var data = JSON.parse(canvas.dataset.histogram || '[]');
|
|
var color = canvas.dataset.color || '#888888';
|
|
|
|
if (!data || data.length === 0) return;
|
|
|
|
var w = canvas.width;
|
|
var h = canvas.height;
|
|
var max = Math.max.apply(null, data);
|
|
var barWidth = w / data.length;
|
|
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
// Draw bars
|
|
ctx.fillStyle = color;
|
|
ctx.globalAlpha = 0.7;
|
|
|
|
for (var i = 0; i < data.length; i++) {
|
|
var barHeight = (data[i] / max) * h;
|
|
ctx.fillRect(i * barWidth, h - barHeight, barWidth - 0.5, barHeight);
|
|
}
|
|
|
|
// Draw outline
|
|
ctx.globalAlpha = 1;
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, h);
|
|
for (var i = 0; i < data.length; i++) {
|
|
var barHeight = (data[i] / max) * h;
|
|
ctx.lineTo(i * barWidth + barWidth / 2, h - barHeight);
|
|
}
|
|
ctx.lineTo(w, h);
|
|
ctx.stroke();
|
|
});
|
|
}
|
|
// Draw histograms when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', drawHistograms);
|
|
} else {
|
|
setTimeout(drawHistograms, 0);
|
|
}
|
|
<\/script>
|
|
<div class="object-viewer">`;
|
|
|
|
for (const obj of objects) {
|
|
html += this.renderObject(obj);
|
|
}
|
|
|
|
html += `</div>`;
|
|
return html;
|
|
}
|
|
|
|
static renderObject(obj) {
|
|
const id = `obj-${Math.random().toString(36).slice(2, 9)}`;
|
|
const categoryIcon = this.getCategoryIcon(obj.category);
|
|
const categoryClass = `category-${obj.category}`;
|
|
|
|
let html = `
|
|
<div class="object-card ${categoryClass}">
|
|
<div class="object-header" onclick="toggleObjectSection('${id}')">
|
|
<span class="object-icon">${categoryIcon}</span>
|
|
<span class="object-type">${escapeHtml(obj.full_type || obj.type_name)}</span>
|
|
<span class="object-category">${escapeHtml(obj.category)}</span>
|
|
<span class="object-toggle">▼</span>
|
|
</div>
|
|
<div class="object-content" id="${id}">
|
|
`;
|
|
|
|
// Metrics Section
|
|
if (obj.metrics && Object.keys(obj.metrics).length > 0) {
|
|
html += this.renderMetrics(obj.metrics, obj.category);
|
|
}
|
|
|
|
// Spectral/Histogram Section
|
|
if (obj.spectral) {
|
|
html += this.renderSpectral(obj.spectral);
|
|
}
|
|
|
|
// Attributes Section (for generic objects)
|
|
if (obj.attributes && Object.keys(obj.attributes).length > 0) {
|
|
html += this.renderAttributes(obj.attributes);
|
|
}
|
|
|
|
// Source Info Section
|
|
if (obj.source_info) {
|
|
html += this.renderSourceInfo(obj.source_info);
|
|
}
|
|
|
|
// Serialized Data Section
|
|
if (obj.serialized) {
|
|
html += this.renderSerialized(obj.serialized);
|
|
}
|
|
|
|
html += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return html;
|
|
}
|
|
|
|
static getCategoryIcon(category) {
|
|
const icons = {
|
|
tensor: "🔢",
|
|
numpy: "📊",
|
|
pil_image: "🖼️",
|
|
safetensors: "📦",
|
|
dict: "📋",
|
|
list: "📝",
|
|
object: "🔮",
|
|
};
|
|
return icons[category] || "❓";
|
|
}
|
|
|
|
static renderMetrics(metrics, category) {
|
|
const id = `metrics-${Math.random().toString(36).slice(2, 9)}`;
|
|
|
|
let html = `
|
|
<div class="object-section">
|
|
<div class="section-header" onclick="toggleSection('${id}')">
|
|
<span class="section-title">📊 Metrics</span>
|
|
<span class="section-toggle">▼</span>
|
|
</div>
|
|
<div class="section-content" id="${id}">
|
|
<div class="metrics-grid">
|
|
`;
|
|
|
|
// Render based on category for optimal display
|
|
if (category === "tensor" || category === "numpy") {
|
|
html += this.renderTensorMetrics(metrics);
|
|
} else if (category === "pil_image") {
|
|
html += this.renderImageMetrics(metrics);
|
|
} else if (category === "safetensors") {
|
|
html += this.renderSafetensorsMetrics(metrics);
|
|
} else {
|
|
html += this.renderGenericMetrics(metrics);
|
|
}
|
|
|
|
html += `
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return html;
|
|
}
|
|
|
|
static renderTensorMetrics(metrics) {
|
|
let html = "";
|
|
|
|
// Primary info row
|
|
if (metrics.shape) {
|
|
html += `<div class="metric-item highlight"><span class="metric-label">Shape</span><span class="metric-value">[${metrics.shape.join(", ")}]</span></div>`;
|
|
}
|
|
if (metrics.dtype) {
|
|
html += `<div class="metric-item"><span class="metric-label">DType</span><span class="metric-value dtype">${escapeHtml(metrics.dtype)}</span></div>`;
|
|
}
|
|
if (metrics.resolution) {
|
|
html += `<div class="metric-item highlight"><span class="metric-label">Resolution</span><span class="metric-value">${escapeHtml(metrics.resolution)}</span></div>`;
|
|
}
|
|
if (metrics.channels) {
|
|
html += `<div class="metric-item"><span class="metric-label">Channels</span><span class="metric-value">${metrics.channels}</span></div>`;
|
|
}
|
|
if (metrics.image_format) {
|
|
html += `<div class="metric-item"><span class="metric-label">Format</span><span class="metric-value">${escapeHtml(metrics.image_format)}</span></div>`;
|
|
}
|
|
if (metrics.memory_human) {
|
|
html += `<div class="metric-item"><span class="metric-label">Memory</span><span class="metric-value">${escapeHtml(metrics.memory_human)}</span></div>`;
|
|
}
|
|
if (metrics.device) {
|
|
html += `<div class="metric-item"><span class="metric-label">Device</span><span class="metric-value device">${escapeHtml(metrics.device)}</span></div>`;
|
|
}
|
|
if (metrics.numel) {
|
|
html += `<div class="metric-item"><span class="metric-label">Elements</span><span class="metric-value">${metrics.numel.toLocaleString()}</span></div>`;
|
|
}
|
|
|
|
// Statistics
|
|
if (metrics.stats) {
|
|
html += `<div class="metric-divider"></div>`;
|
|
html += `<div class="metric-item stat"><span class="metric-label">Min</span><span class="metric-value">${this.formatNumber(metrics.stats.min)}</span></div>`;
|
|
html += `<div class="metric-item stat"><span class="metric-label">Max</span><span class="metric-value">${this.formatNumber(metrics.stats.max)}</span></div>`;
|
|
html += `<div class="metric-item stat"><span class="metric-label">Mean</span><span class="metric-value">${this.formatNumber(metrics.stats.mean)}</span></div>`;
|
|
html += `<div class="metric-item stat"><span class="metric-label">Std</span><span class="metric-value">${this.formatNumber(metrics.stats.std)}</span></div>`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
static renderImageMetrics(metrics) {
|
|
let html = "";
|
|
|
|
if (metrics.resolution) {
|
|
html += `<div class="metric-item highlight"><span class="metric-label">Resolution</span><span class="metric-value">${escapeHtml(metrics.resolution)}</span></div>`;
|
|
}
|
|
if (metrics.mode) {
|
|
html += `<div class="metric-item"><span class="metric-label">Mode</span><span class="metric-value">${escapeHtml(metrics.mode)}</span></div>`;
|
|
}
|
|
if (metrics.channels) {
|
|
html += `<div class="metric-item"><span class="metric-label">Channels</span><span class="metric-value">${metrics.channels}</span></div>`;
|
|
}
|
|
if (metrics.bands) {
|
|
html += `<div class="metric-item"><span class="metric-label">Bands</span><span class="metric-value">${metrics.bands.join(", ")}</span></div>`;
|
|
}
|
|
if (metrics.format) {
|
|
html += `<div class="metric-item"><span class="metric-label">Format</span><span class="metric-value">${escapeHtml(metrics.format || "N/A")}</span></div>`;
|
|
}
|
|
if (metrics.memory_human) {
|
|
html += `<div class="metric-item"><span class="metric-label">Memory</span><span class="metric-value">${escapeHtml(metrics.memory_human)}</span></div>`;
|
|
}
|
|
if (metrics.dpi) {
|
|
html += `<div class="metric-item"><span class="metric-label">DPI</span><span class="metric-value">${metrics.dpi}</span></div>`;
|
|
}
|
|
if (metrics.has_icc_profile) {
|
|
html += `<div class="metric-item"><span class="metric-label">ICC Profile</span><span class="metric-value">✓</span></div>`;
|
|
}
|
|
if (metrics.has_exif) {
|
|
html += `<div class="metric-item"><span class="metric-label">EXIF</span><span class="metric-value">✓</span></div>`;
|
|
}
|
|
|
|
// Statistics
|
|
if (metrics.stats) {
|
|
html += `<div class="metric-divider"></div>`;
|
|
html += `<div class="metric-item stat"><span class="metric-label">Min</span><span class="metric-value">${metrics.stats.min}</span></div>`;
|
|
html += `<div class="metric-item stat"><span class="metric-label">Max</span><span class="metric-value">${metrics.stats.max}</span></div>`;
|
|
html += `<div class="metric-item stat"><span class="metric-label">Mean</span><span class="metric-value">${this.formatNumber(metrics.stats.mean)}</span></div>`;
|
|
html += `<div class="metric-item stat"><span class="metric-label">Std</span><span class="metric-value">${this.formatNumber(metrics.stats.std)}</span></div>`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
static renderSafetensorsMetrics(metrics) {
|
|
let html = "";
|
|
|
|
if (metrics.tensor_count !== undefined) {
|
|
html += `<div class="metric-item highlight"><span class="metric-label">Tensors</span><span class="metric-value">${metrics.tensor_count.toLocaleString()}</span></div>`;
|
|
}
|
|
if (metrics.total_params_human) {
|
|
html += `<div class="metric-item highlight"><span class="metric-label">Parameters</span><span class="metric-value">${escapeHtml(metrics.total_params_human)}</span></div>`;
|
|
}
|
|
|
|
// Tensor info table
|
|
if (metrics.tensor_info && metrics.tensor_info.length > 0) {
|
|
html += `<div class="metric-divider"></div>`;
|
|
html += `<div class="tensor-table-wrapper">`;
|
|
html += `<table class="tensor-table">`;
|
|
html += `<thead><tr><th>Name</th><th>Shape</th><th>DType</th><th>Params</th></tr></thead>`;
|
|
html += `<tbody>`;
|
|
|
|
for (const tensor of metrics.tensor_info) {
|
|
if (tensor.truncated) {
|
|
html += `<tr class="truncated-row"><td colspan="4">${escapeHtml(tensor.name)}</td></tr>`;
|
|
} else if (tensor.error) {
|
|
html += `<tr class="error-row"><td>${escapeHtml(tensor.name)}</td><td colspan="3" class="error">${escapeHtml(tensor.error)}</td></tr>`;
|
|
} else {
|
|
const shape = tensor.shape ? `[${tensor.shape.join(", ")}]` : "?";
|
|
const params = tensor.params ? tensor.params.toLocaleString() : "?";
|
|
html += `<tr><td class="tensor-name">${escapeHtml(tensor.name)}</td><td>${shape}</td><td>${escapeHtml(tensor.dtype || "?")}</td><td>${params}</td></tr>`;
|
|
}
|
|
}
|
|
|
|
html += `</tbody></table>`;
|
|
html += `</div>`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
static renderGenericMetrics(metrics) {
|
|
let html = "";
|
|
|
|
for (const [key, value] of Object.entries(metrics)) {
|
|
if (key === "error") continue;
|
|
|
|
let displayValue;
|
|
if (Array.isArray(value)) {
|
|
displayValue = value.slice(0, 10).join(", ");
|
|
if (value.length > 10) displayValue += "...";
|
|
} else if (typeof value === "object" && value !== null) {
|
|
displayValue = JSON.stringify(value).slice(0, 100);
|
|
} else {
|
|
displayValue = String(value);
|
|
}
|
|
|
|
html += `<div class="metric-item"><span class="metric-label">${escapeHtml(this.formatLabel(key))}</span><span class="metric-value">${escapeHtml(displayValue)}</span></div>`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
static renderSpectral(spectral) {
|
|
const id = `spectral-${Math.random().toString(36).slice(2, 9)}`;
|
|
|
|
let html = `
|
|
<div class="object-section spectral-section">
|
|
<div class="section-header" onclick="toggleSection('${id}')">
|
|
<span class="section-title">📈 ${spectral.type === "spectral" ? "Channel Histograms" : "Histogram"}</span>
|
|
<span class="section-toggle">▼</span>
|
|
</div>
|
|
<div class="section-content" id="${id}">
|
|
`;
|
|
|
|
if (spectral.type === "spectral" && spectral.channels) {
|
|
// Multi-channel spectral view
|
|
html += `<div class="spectral-container">`;
|
|
|
|
for (const channel of spectral.channels) {
|
|
const canvasId = `canvas-${Math.random().toString(36).slice(2, 9)}`;
|
|
const color = this.getChannelColor(channel.name);
|
|
|
|
html += `
|
|
<div class="channel-histogram">
|
|
<div class="channel-label" style="color: ${color}">${escapeHtml(channel.name)}</div>
|
|
<canvas id="${canvasId}" class="histogram-canvas" width="200" height="60"
|
|
data-histogram='${JSON.stringify(channel.histogram)}'
|
|
data-color="${color}"></canvas>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
html += `</div>`;
|
|
} else if (spectral.data) {
|
|
// Single histogram
|
|
const canvasId = `canvas-${Math.random().toString(36).slice(2, 9)}`;
|
|
html += `
|
|
<div class="single-histogram">
|
|
<canvas id="${canvasId}" class="histogram-canvas wide" width="400" height="80"
|
|
data-histogram='${JSON.stringify(spectral.data)}'
|
|
data-color="#888888"></canvas>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
html += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return html;
|
|
}
|
|
|
|
static getChannelColor(name) {
|
|
const colors = {
|
|
R: "#ff4444",
|
|
G: "#44ff44",
|
|
B: "#4488ff",
|
|
A: "#ffffff",
|
|
L: "#888888",
|
|
};
|
|
return colors[name] || "#888888";
|
|
}
|
|
|
|
static renderAttributes(attributes) {
|
|
const id = `attrs-${Math.random().toString(36).slice(2, 9)}`;
|
|
|
|
let html = `
|
|
<div class="object-section">
|
|
<div class="section-header" onclick="toggleSection('${id}')">
|
|
<span class="section-title">🏷️ Attributes (${Object.keys(attributes).length})</span>
|
|
<span class="section-toggle">▼</span>
|
|
</div>
|
|
<div class="section-content collapsed" id="${id}">
|
|
<table class="attributes-table">
|
|
<thead><tr><th>Name</th><th>Type</th><th>Value</th></tr></thead>
|
|
<tbody>
|
|
`;
|
|
|
|
for (const [name, info] of Object.entries(attributes)) {
|
|
html += `<tr>
|
|
<td class="attr-name">${escapeHtml(name)}</td>
|
|
<td class="attr-type">${escapeHtml(info.type)}</td>
|
|
<td class="attr-value">${escapeHtml(String(info.value))}</td>
|
|
</tr>`;
|
|
}
|
|
|
|
html += `
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return html;
|
|
}
|
|
|
|
static renderSourceInfo(sourceInfo) {
|
|
const id = `source-${Math.random().toString(36).slice(2, 9)}`;
|
|
|
|
let html = `
|
|
<div class="object-section">
|
|
<div class="section-header" onclick="toggleSection('${id}')">
|
|
<span class="section-title">📁 Source Info</span>
|
|
<span class="section-toggle">▼</span>
|
|
</div>
|
|
<div class="section-content collapsed" id="${id}">
|
|
<div class="source-info">
|
|
`;
|
|
|
|
if (sourceInfo.file) {
|
|
html += `<div class="source-item"><span class="source-label">File:</span><span class="source-value path">${escapeHtml(sourceInfo.file)}</span></div>`;
|
|
}
|
|
if (sourceInfo.source_lines) {
|
|
html += `<div class="source-item"><span class="source-label">Lines:</span><span class="source-value">${sourceInfo.source_lines}</span></div>`;
|
|
}
|
|
if (sourceInfo.bases && sourceInfo.bases.length > 0) {
|
|
html += `<div class="source-item"><span class="source-label">Bases:</span><span class="source-value">${sourceInfo.bases.join(" → ")}</span></div>`;
|
|
}
|
|
|
|
html += `
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return html;
|
|
}
|
|
|
|
static renderSerialized(serialized) {
|
|
const id = `serial-${Math.random().toString(36).slice(2, 9)}`;
|
|
|
|
let displayContent;
|
|
try {
|
|
const parsed = JSON.parse(serialized);
|
|
displayContent = JSON.stringify(parsed, null, 2);
|
|
} catch {
|
|
displayContent = serialized;
|
|
}
|
|
|
|
// Truncate if too long
|
|
const maxLen = 5000;
|
|
let truncated = false;
|
|
if (displayContent.length > maxLen) {
|
|
displayContent = displayContent.slice(0, maxLen);
|
|
truncated = true;
|
|
}
|
|
|
|
return `
|
|
<div class="object-section">
|
|
<div class="section-header" onclick="toggleSection('${id}')">
|
|
<span class="section-title">📄 Serialized Data${truncated ? " (truncated)" : ""}</span>
|
|
<span class="section-toggle">▼</span>
|
|
</div>
|
|
<div class="section-content collapsed" id="${id}">
|
|
<pre class="serialized-content">${escapeHtml(displayContent)}${truncated ? "\n\n... (truncated)" : ""}</pre>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
static formatNumber(num) {
|
|
if (typeof num !== "number") return String(num);
|
|
if (Number.isInteger(num)) return num.toLocaleString();
|
|
if (Math.abs(num) < 0.001 || Math.abs(num) > 100000) {
|
|
return num.toExponential(3);
|
|
}
|
|
return num.toFixed(4);
|
|
}
|
|
|
|
static formatLabel(key) {
|
|
return key
|
|
.replace(/_/g, " ")
|
|
.replace(/([A-Z])/g, " $1")
|
|
.trim()
|
|
.split(" ")
|
|
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
.join(" ");
|
|
}
|
|
|
|
static getStyles(theme) {
|
|
return `
|
|
.object-viewer {
|
|
padding: 12px;
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.object-card {
|
|
background: ${theme.bg};
|
|
border: 1px solid ${theme.border};
|
|
border-radius: 8px;
|
|
margin-bottom: 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.object-card.category-tensor { border-left: 3px solid #ff9800; }
|
|
.object-card.category-numpy { border-left: 3px solid #2196f3; }
|
|
.object-card.category-pil_image { border-left: 3px solid #4caf50; }
|
|
.object-card.category-safetensors { border-left: 3px solid #9c27b0; }
|
|
.object-card.category-dict { border-left: 3px solid #607d8b; }
|
|
.object-card.category-list { border-left: 3px solid #795548; }
|
|
.object-card.category-object { border-left: 3px solid #00bcd4; }
|
|
|
|
.object-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 14px;
|
|
background: linear-gradient(to right, rgba(255,255,255,0.03), transparent);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.object-header:hover { background: rgba(255,255,255,0.05); }
|
|
|
|
.object-icon { font-size: 18px; }
|
|
.object-type {
|
|
font-family: 'SF Mono', Consolas, monospace;
|
|
font-weight: 600;
|
|
color: ${theme.accent || '#4fc3f7'};
|
|
flex: 1;
|
|
}
|
|
.object-category {
|
|
font-size: 11px;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
background: rgba(255,255,255,0.1);
|
|
color: ${theme.fg}88;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.object-toggle {
|
|
font-size: 10px;
|
|
color: ${theme.fg}66;
|
|
transition: transform 0.2s;
|
|
}
|
|
.object-content.collapsed + .object-header .object-toggle,
|
|
.object-content.collapsed ~ .object-toggle { transform: rotate(-90deg); }
|
|
|
|
.object-content {
|
|
border-top: 1px solid ${theme.border};
|
|
}
|
|
.object-content.collapsed { display: none; }
|
|
|
|
.object-section {
|
|
border-bottom: 1px solid ${theme.border}44;
|
|
}
|
|
.object-section:last-child { border-bottom: none; }
|
|
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 14px;
|
|
background: rgba(0,0,0,0.1);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.section-header:hover { background: rgba(0,0,0,0.15); }
|
|
|
|
.section-title {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: ${theme.fg}cc;
|
|
}
|
|
.section-toggle {
|
|
font-size: 9px;
|
|
color: ${theme.fg}66;
|
|
transition: transform 0.2s;
|
|
}
|
|
.section-content.collapsed + .section-header .section-toggle { transform: rotate(-90deg); }
|
|
|
|
.section-content {
|
|
padding: 12px 14px;
|
|
}
|
|
.section-content.collapsed { display: none; }
|
|
|
|
/* Metrics Grid */
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
gap: 8px;
|
|
}
|
|
.metric-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 8px 10px;
|
|
background: rgba(255,255,255,0.03);
|
|
border-radius: 6px;
|
|
border: 1px solid ${theme.border}33;
|
|
}
|
|
.metric-item.highlight {
|
|
background: rgba(79, 195, 247, 0.08);
|
|
border-color: rgba(79, 195, 247, 0.3);
|
|
}
|
|
.metric-item.stat {
|
|
background: rgba(156, 39, 176, 0.08);
|
|
border-color: rgba(156, 39, 176, 0.2);
|
|
}
|
|
.metric-label {
|
|
font-size: 10px;
|
|
color: ${theme.fg}88;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 2px;
|
|
}
|
|
.metric-value {
|
|
font-family: 'SF Mono', Consolas, monospace;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: ${theme.fg};
|
|
word-break: break-all;
|
|
}
|
|
.metric-value.dtype { color: #ff9800; }
|
|
.metric-value.device { color: #4caf50; }
|
|
.metric-divider {
|
|
grid-column: 1 / -1;
|
|
height: 1px;
|
|
background: ${theme.border}44;
|
|
margin: 4px 0;
|
|
}
|
|
|
|
/* Tensor Table */
|
|
.tensor-table-wrapper {
|
|
grid-column: 1 / -1;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
border-radius: 6px;
|
|
border: 1px solid ${theme.border}44;
|
|
}
|
|
.tensor-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 11px;
|
|
}
|
|
.tensor-table th {
|
|
position: sticky;
|
|
top: 0;
|
|
background: ${theme.bg};
|
|
padding: 6px 8px;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
color: ${theme.fg}aa;
|
|
border-bottom: 1px solid ${theme.border};
|
|
}
|
|
.tensor-table td {
|
|
padding: 4px 8px;
|
|
border-bottom: 1px solid ${theme.border}22;
|
|
font-family: 'SF Mono', Consolas, monospace;
|
|
}
|
|
.tensor-table .tensor-name {
|
|
max-width: 200px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: ${theme.accent || '#4fc3f7'};
|
|
}
|
|
.tensor-table .truncated-row td { color: ${theme.fg}66; font-style: italic; }
|
|
.tensor-table .error-row .error { color: #f44336; }
|
|
|
|
/* Spectral/Histogram */
|
|
.spectral-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
.channel-histogram {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.channel-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
.histogram-canvas {
|
|
background: rgba(0,0,0,0.2);
|
|
border-radius: 4px;
|
|
border: 1px solid ${theme.border}44;
|
|
}
|
|
.histogram-canvas.wide {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
}
|
|
.single-histogram {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* Attributes Table */
|
|
.attributes-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 12px;
|
|
}
|
|
.attributes-table th {
|
|
text-align: left;
|
|
padding: 6px 8px;
|
|
background: rgba(0,0,0,0.15);
|
|
color: ${theme.fg}aa;
|
|
font-weight: 600;
|
|
}
|
|
.attributes-table td {
|
|
padding: 4px 8px;
|
|
border-bottom: 1px solid ${theme.border}22;
|
|
vertical-align: top;
|
|
}
|
|
.attributes-table .attr-name {
|
|
font-family: 'SF Mono', Consolas, monospace;
|
|
color: ${theme.accent || '#4fc3f7'};
|
|
white-space: nowrap;
|
|
}
|
|
.attributes-table .attr-type {
|
|
font-family: 'SF Mono', Consolas, monospace;
|
|
color: #ff9800;
|
|
font-size: 11px;
|
|
}
|
|
.attributes-table .attr-value {
|
|
font-family: 'SF Mono', Consolas, monospace;
|
|
color: ${theme.fg}cc;
|
|
max-width: 300px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Source Info */
|
|
.source-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.source-item {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
.source-label {
|
|
font-size: 11px;
|
|
color: ${theme.fg}88;
|
|
min-width: 50px;
|
|
}
|
|
.source-value {
|
|
font-family: 'SF Mono', Consolas, monospace;
|
|
font-size: 12px;
|
|
color: ${theme.fg};
|
|
word-break: break-all;
|
|
}
|
|
.source-value.path { color: #4caf50; }
|
|
|
|
/* Serialized Content */
|
|
.serialized-content {
|
|
background: rgba(0,0,0,0.2);
|
|
padding: 12px;
|
|
border-radius: 6px;
|
|
font-family: 'SF Mono', Consolas, monospace;
|
|
font-size: 11px;
|
|
line-height: 1.5;
|
|
overflow-x: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
margin: 0;
|
|
color: ${theme.fg}dd;
|
|
}
|
|
|
|
/* Scrollbar */
|
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { background: ${theme.fg}33; border-radius: 4px; }
|
|
::-webkit-scrollbar-thumb:hover { background: ${theme.fg}55; }
|
|
`;
|
|
}
|
|
|
|
// Scripts are included inline in render() to ensure they're available immediately
|
|
static getScripts() {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
export default ObjectView;
|