"""generate_report.py 的静态 HTML 资源:CSS 样式与 JS 模板常量。
从 generate_report.py 分离,避免单文件过大;内容逐字节保持不变。
"""
HTML_CSS = '''
<style>
:root {
--bg-primary: #1e1e1e;
--bg-secondary: #252526;
--bg-tertiary: #2d2d2d;
--bg-hover: #2d2d2d;
--text-primary: #d4d4d4;
--text-secondary: #808080;
--accent-blue: #4fc1ff;
--accent-green: #6a9955;
--accent-orange: #ce9178;
--border-color: #3c3c3c;
--button-bg: #0e639c;
--button-hover: #1177bb;
--tooltip-bg: #1e1e2e;
--tooltip-border: #45475a;
}
[data-theme="dracula"] {
--bg-primary: #282a36;
--bg-secondary: #21222c;
--bg-tertiary: #343746;
--bg-hover: #44475a;
--text-primary: #f8f8f2;
--text-secondary: #6272a4;
--accent-blue: #8be9fd;
--accent-green: #50fa7b;
--accent-orange: #ffb86c;
--border-color: #44475a;
--button-bg: #bd93f9;
--button-hover: #ff79c6;
--tooltip-bg: #1d1e26;
--tooltip-border: #6272a4;
}
[data-theme="one-dark"] {
--bg-primary: #282c34;
--bg-secondary: #21252b;
--bg-tertiary: #2c313a;
--bg-hover: #2c313a;
--text-primary: #abb2bf;
--text-secondary: #5c6370;
--accent-blue: #61afef;
--accent-green: #98c379;
--accent-orange: #d19a66;
--border-color: #181a1f;
--button-bg: #4d78cc;
--button-hover: #528bff;
--tooltip-bg: #21252b;
--tooltip-border: #5c6370;
}
[data-theme="github-light"] {
--bg-primary: #ffffff;
--bg-secondary: #f6f8fa;
--bg-tertiary: #f0f2f5;
--bg-hover: #eaeef2;
--text-primary: #1f2328;
--text-secondary: #656d76;
--accent-blue: #0969da;
--accent-green: #1a7f37;
--accent-orange: #bc4c00;
--border-color: #d1d9e0;
--button-bg: #0969da;
--button-hover: #0550ae;
--tooltip-bg: #f6f8fa;
--tooltip-border: #d1d9e0;
}
[data-theme="solarized-light"] {
--bg-primary: #fdf6e3;
--bg-secondary: #eee8d5;
--bg-tertiary: #e8e1cc;
--bg-hover: #e0dac7;
--text-primary: #073642;
--text-secondary: #839496;
--accent-blue: #268bd2;
--accent-green: #859900;
--accent-orange: #cb4b16;
--border-color: #d3cbb7;
--button-bg: #268bd2;
--button-hover: #1a6da8;
--tooltip-bg: #eee8d5;
--tooltip-border: #839496;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', 'SF Pro Display', -apple-system, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
padding: 40px;
max-width: 1600px;
margin: 0 auto;
}
h1 { font-size: 28px; margin-bottom: 20px; color: var(--text-primary);
border-bottom: 2px solid var(--border-color); padding-bottom: 15px; }
.metadata {
background: var(--bg-tertiary);
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.meta-row { display: flex; flex-wrap: wrap; gap: 25px; }
.meta-item { display: flex; gap: 8px; align-items: center; }
.meta-label { color: var(--text-secondary); font-size: 13px; }
.meta-value { color: var(--text-primary); font-family: 'Consolas', monospace; font-size: 13px; }
.meta-datasources { display: flex; flex-direction: column; gap: 4px; margin-left: 20px; }
.meta-datasource-item {
display: flex;
gap: 12px;
align-items: baseline;
color: var(--text-primary);
font-family: 'Consolas', monospace;
font-size: 12px;
}
.meta-datasource-item .step-badge {
background: var(--button-bg);
color: #fff;
padding: 1px 6px;
border-radius: 3px;
font-size: 11px;
}
.kernel-fields-config {
position: relative;
display: inline-block;
}
.kernel-fields-panel {
position: absolute;
top: 100%;
left: 0;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
min-width: 200px;
z-index: 1000;
display: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
margin-top: 4px;
}
.kernel-fields-panel.visible {
display: block;
}
.kernel-fields-panel label {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
cursor: pointer;
font-size: 13px;
color: var(--text-primary);
}
.kernel-fields-panel input[type="checkbox"] {
cursor: pointer;
}
.kernel-fields-actions {
display: flex;
gap: 8px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border-color);
}
.kernel-fields-actions button {
flex: 1;
padding: 4px 8px;
font-size: 11px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 3px;
cursor: pointer;
}
.kernel-fields-actions button:hover {
background: var(--bg-hover);
}
.controls {
background: var(--bg-tertiary);
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.controls label { display: flex; align-items: center; gap: 8px; color: var(--text-secondary); font-size: 13px; }
.controls select {
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 6px 12px;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
}
.controls button {
background: var(--button-bg);
color: #ffffff;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: background 0.2s;
}
.controls button:hover { background: var(--button-hover); }
.tree { background: var(--bg-secondary); border-radius: 8px; padding: 20px; position: relative; }
.tree-root { list-style: none; }
.tree-children { list-style: none; padding-left: 24px; overflow: hidden; transition: max-height 0.25s ease-out; }
.tree-node.collapsed > .tree-children { max-height: 0; }
.tree-node.expanded > .tree-children { max-height: 50000px; }
.node-header {
display: flex;
align-items: center;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s;
flex-wrap: wrap;
}
.node-header:hover { background: var(--bg-hover); }
.node-toggle {
width: 16px;
font-size: 10px;
color: var(--text-secondary);
text-align: center;
flex-shrink: 0;
transition: transform 0.2s;
}
.tree-node.collapsed > .node-header .node-toggle { transform: rotate(0deg); }
.tree-node.expanded > .node-header .node-toggle { transform: rotate(90deg); }
.tree-node.leaf > .node-header .node-toggle { visibility: hidden; }
.tree-node.kernel-only > .node-header .node-toggle { visibility: hidden; }
.node-name {
flex: 1;
margin-left: 8px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
min-width: 150px;
}
.node-semantic {
color: var(--accent-orange);
font-size: 11px;
font-style: italic;
font-weight: normal;
margin-left: 6px;
}
.node-semantic-wrapper {
display: inline-flex;
align-items: center;
color: var(--accent-orange);
font-size: 11px;
font-style: italic;
margin-left: 6px;
}
.node-semantic-truncated {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 400px;
}
.node-semantic-full {
display: none;
}
.node-semantic-wrapper.expanded .node-semantic-truncated {
display: none;
}
.node-semantic-wrapper.expanded .node-semantic-full {
display: inline;
white-space: normal;
}
.semantic-expand-btn {
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-left: 4px;
flex-shrink: 0;
user-select: none;
width: 18px;
height: 18px;
border: none;
border-radius: 3px;
background: var(--accent-orange);
padding: 0;
transition: background 0.15s;
}
.semantic-expand-btn svg {
width: 12px;
height: 12px;
fill: var(--bg-primary);
transition: transform 0.2s;
}
.node-semantic-wrapper.expanded .semantic-expand-btn {
background: var(--accent-blue);
}
.node-semantic-wrapper.expanded .semantic-expand-btn svg {
transform: rotate(180deg);
}
.semantic-expand-btn:hover {
opacity: 0.85;
}
.kernel-semantic {
color: var(--accent-orange);
font-size: 11px;
font-style: italic;
margin-left: 4px;
}
.kernel-count, .stream-count {
color: var(--text-secondary);
font-size: 11px;
margin-left: 4px;
}
.tree-node.hidden-kernel {
display: none;
}
.kernel-meta {
color: var(--text-secondary);
font-size: 11px;
margin-left: 28px;
width: calc(100% - 28px);
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2px 16px;
font-family: 'Consolas', monospace;
}
.kernel-meta-item {
display: flex;
align-items: flex-start;
gap: 4px;
min-width: 0;
}
.kernel-meta-label {
color: var(--text-secondary);
flex-shrink: 0;
}
.kernel-meta-value {
color: var(--text-primary);
word-break: break-all;
overflow-wrap: break-word;
}
.kernel-meta-item.shape-semantic-tip {
position: relative;
cursor: help;
}
.kernel-meta-item.shape-semantic-tip::after {
content: attr(data-shape-semantic);
position: absolute;
bottom: calc(100% + 5px);
left: 0;
background: rgba(20,22,34,0.97);
color: #e8c77a;
border: 1px solid rgba(232,199,122,0.35);
padding: 5px 9px;
border-radius: 5px;
font-size: 11px;
font-family: 'Consolas', monospace;
white-space: normal;
max-width: 480px;
min-width: 180px;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 300;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
line-height: 1.5;
}
.kernel-meta-item.shape-semantic-tip:hover::after {
opacity: 1;
}
.node-duration {
text-align: right;
min-width: 200px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
display: flex;
justify-content: flex-end;
gap: 6px;
}
.duration-us { color: var(--accent-green); }
.duration-ms { color: var(--accent-orange); font-size: 11px; }
.duration-pct { color: var(--accent-blue); font-weight: bold; min-width: 65px; }
.tree-node[data-type="kernel"] .node-name {
color: var(--accent-blue);
font-style: italic;
font-size: 13px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tree-node[data-type="kernel"] .node-duration {
margin-left: auto;
}
.kernel-info-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background: var(--accent-blue);
color: #fff;
border-radius: 50%;
cursor: pointer;
margin-left: 4px;
flex-shrink: 0;
user-select: none;
border-bottom: none;
padding: 0;
border: none;
}
.kernel-info-btn svg {
width: 12px;
height: 12px;
fill: #fff;
}
.kernel-info-btn:hover {
opacity: 0.8;
}
.node-header[title] { cursor: help; }
.node-spacer { width: 16px; flex-shrink: 0; }
.tree-node.leaf > .node-header { cursor: pointer; }
/* Kernel Tooltip Styles */
.kernel-tooltip {
position: fixed;
background: var(--tooltip-bg);
border: 1px solid var(--tooltip-border);
border-radius: 8px;
padding: 12px 16px;
max-width: 1200px;
max-height: 80vh;
overflow-y: auto;
z-index: 10000;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
font-size: 12px;
display: none;
}
.kernel-tooltip.visible { display: block; }
.kernel-tooltip-header {
font-weight: bold;
font-size: 14px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--tooltip-border);
color: var(--accent-blue);
word-break: break-all;
}
.kernel-tooltip-semantic {
background: rgba(79, 193, 255, 0.1);
border-left: 3px solid var(--accent-blue);
padding: 6px 10px;
margin-bottom: 10px;
font-style: italic;
color: var(--text-secondary);
}
.kernel-tooltip-container {
width: 100%;
}
.kernel-tooltip-row {
display: flex;
gap: 16px;
margin-bottom: 8px;
}
.kernel-tooltip-row:last-child {
margin-bottom: 0;
}
.kernel-tooltip-group {
flex: 1;
min-width: 160px;
}
.kernel-tooltip-group-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 2px 8px;
}
.kernel-tooltip-label {
font-weight: 500;
color: var(--text-secondary);
white-space: nowrap;
font-size: 11px;
}
.kernel-tooltip-value {
color: var(--text-primary);
word-break: break-all;
font-size: 11px;
}
.kernel-tooltip-section {
font-weight: bold;
color: var(--accent-orange);
padding: 6px 0 4px 0;
font-size: 12px;
border-bottom: 1px solid var(--tooltip-border);
margin-bottom: 4px;
}
.kernel-tooltip-full {
grid-column: 1 / -1;
display: flex;
gap: 8px;
}
.kernel-tooltip-full .kernel-tooltip-label {
flex-shrink: 0;
}
/* Timeline Styles - Module Schematic View */
.timeline-section {
background: var(--bg-secondary);
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.timeline-header h2 {
font-size: 18px;
color: var(--text-primary);
margin: 0;
}
.timeline-hint {
font-size: 12px;
color: var(--text-secondary);
}
.timeline-container {
position: relative;
overflow-x: auto;
overflow-y: visible;
}
.timeline-overview {
margin: 16px 0;
}
.timeline-overview-title {
font-size: 14px;
color: var(--text-primary);
margin-bottom: 12px;
font-weight: 500;
}
.timeline-overview-list {
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 8px;
background: var(--bg-tertiary);
}
.timeline-node-item {
display: flex;
align-items: center;
padding: 10px 12px;
margin: 4px 0;
background: var(--bg-secondary);
border-radius: 6px;
cursor: pointer;
transition: background 0.15s;
}
.timeline-node-item:hover {
background: var(--bg-hover);
}
.timeline-node-item.has-children:hover {
background: rgba(79, 193, 255, 0.1);
}
.node-expand-icon {
width: 20px;
font-size: 12px;
color: var(--text-secondary);
text-align: center;
}
.timeline-node-item.has-children .node-expand-icon {
color: var(--accent-blue);
}
.node-name {
flex: 1;
font-weight: 500;
color: var(--text-primary);
}
.node-streams {
font-size: 11px;
color: var(--text-secondary);
margin: 0 12px;
white-space: nowrap;
}
.node-duration {
font-size: 11px;
color: var(--accent-green);
margin-right: 12px;
white-space: nowrap;
}
.node-kernels {
font-size: 11px;
color: var(--text-secondary);
white-space: nowrap;
}
.timeline-expand-area {
margin: 16px 0;
padding: 16px;
background: var(--bg-tertiary);
border-radius: 8px;
border: 2px solid var(--accent-blue);
}
.expand-header {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.expand-close {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 4px 12px;
color: var(--text-secondary);
cursor: pointer;
font-size: 12px;
margin-right: 12px;
}
.expand-close:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.expand-breadcrumb {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 0;
overflow-x: auto;
}
.breadcrumb-item {
font-size: 12px;
color: var(--accent-blue);
cursor: pointer;
white-space: nowrap;
padding: 2px 6px;
border-radius: 3px;
transition: background 0.15s;
}
.breadcrumb-item:hover {
background: rgba(79, 193, 255, 0.1);
}
.breadcrumb-item.current {
color: var(--text-primary);
font-weight: 600;
cursor: default;
}
.breadcrumb-item.current:hover {
background: transparent;
}
.breadcrumb-sep {
color: var(--text-secondary);
font-size: 10px;
flex-shrink: 0;
}
.expand-title {
font-weight: bold;
color: var(--accent-blue);
font-size: 14px;
}
.expand-gantt {
margin: 16px 0;
}
.gantt-header {
font-size: 13px;
color: var(--text-primary);
margin-bottom: 8px;
}
.gantt-hint {
font-size: 11px;
color: var(--text-secondary);
}
.gantt-container {
background: var(--bg-secondary);
border-radius: 6px;
padding: 12px;
border: 1px solid var(--border-color);
}
.gantt-stream-group {
margin: 8px 0;
}
.gantt-stream-row {
display: flex;
align-items: center;
min-height: 32px;
}
.gantt-stream-label {
width: 90px;
font-size: 11px;
color: var(--text-secondary);
flex-shrink: 0;
}
.gantt-bars {
flex: 1;
position: relative;
height: 28px;
background: rgba(0,0,0,0.15);
border-radius: 4px;
overflow: visible;
}
.gantt-bar {
position: absolute;
height: 20px;
top: 4px;
border-radius: 3px;
cursor: pointer;
transition: opacity 0.15s, box-shadow 0.15s;
}
.gantt-bar:hover {
opacity: 0.85;
z-index: 10;
box-shadow: 0 0 8px rgba(255,255,255,0.4);
}
.gantt-bar-secondary {
height: 16px;
top: 6px;
}
.gantt-bar-arrow {
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid;
}
.gantt-labels-row {
position: relative;
min-height: 22px;
margin-left: 90px;
margin-top: 2px;
}
.gantt-label {
position: absolute;
font-size: 10px;
white-space: nowrap;
transform: translateX(-50%);
padding: 1px 5px;
border-radius: 3px;
background: var(--bg-tertiary);
}
.gantt-label::before {
content: '';
position: absolute;
top: -4px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-bottom: 4px solid var(--bg-tertiary);
}
.gantt-time-axis {
display: flex;
justify-content: space-between;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border-color);
font-size: 10px;
color: var(--text-secondary);
}
.expand-tree {
margin-top: 16px;
}
.expand-tree-title {
font-size: 13px;
color: var(--text-primary);
margin-bottom: 8px;
}
.tree-hint {
font-size: 11px;
color: var(--text-secondary);
}
.expand-tree-content {
background: var(--bg-secondary);
border-radius: 6px;
padding: 8px 12px;
border: 1px solid var(--border-color);
}
.tree-item {
display: flex;
align-items: center;
padding: 6px 8px;
margin: 2px 0;
border-radius: 4px;
cursor: default;
transition: background 0.15s;
}
.tree-item.clickable {
cursor: pointer;
}
.tree-item.clickable:hover {
background: rgba(79, 193, 255, 0.1);
}
.tree-icon {
width: 20px;
font-size: 12px;
color: var(--text-secondary);
text-align: center;
flex-shrink: 0;
}
.tree-item.clickable .tree-icon {
color: var(--accent-blue);
}
.tree-name {
flex: 1;
font-size: 12px;
color: var(--text-primary);
}
.tree-streams {
font-size: 10px;
color: var(--text-secondary);
margin: 0 8px;
white-space: nowrap;
}
.tree-duration {
font-size: 10px;
color: var(--accent-green);
white-space: nowrap;
}
.timeline-tooltip {
position: fixed;
background: var(--tooltip-bg);
border: 1px solid var(--tooltip-border);
border-radius: 6px;
padding: 10px 12px;
font-size: 12px;
z-index: 10000;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
max-width: 400px;
display: none;
}
.timeline-tooltip.visible {
display: block;
}
.timeline-tooltip-title {
font-weight: bold;
color: var(--accent-blue);
margin-bottom: 6px;
}
.timeline-tooltip-row {
display: flex;
justify-content: space-between;
gap: 20px;
margin-bottom: 3px;
color: var(--text-primary);
}
.timeline-tooltip-label {
color: var(--text-secondary);
}
.timeline-tooltip-streams {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid var(--tooltip-border);
font-size: 11px;
}
.timeline-detail-panel {
margin-top: 15px;
background: var(--bg-tertiary);
border-radius: 6px;
padding: 15px;
display: none;
}
.timeline-detail-panel.visible {
display: block;
}
.timeline-detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.timeline-detail-title {
font-weight: bold;
color: var(--accent-blue);
}
.timeline-detail-close {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 18px;
line-height: 1;
}
.timeline-detail-close:hover {
color: var(--text-primary);
}
.timeline-detail-ops {
background: var(--bg-secondary);
border-radius: 4px;
padding: 12px;
}
.timeline-stream-row {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--border-color);
gap: 6px;
}
.timeline-stream-row:last-child {
border-bottom: none;
}
.timeline-stream-label {
width: 100px;
flex-shrink: 0;
font-size: 11px;
font-weight: 500;
padding: 4px 8px;
border-radius: 3px;
text-align: center;
}
.timeline-stream-ops {
flex: 1;
position: relative;
height: 26px;
background: rgba(0,0,0,0.12);
border-radius: 4px;
}
.timeline-detail-bar {
position: absolute;
height: 22px;
top: 2px;
border-radius: 3px;
font-size: 9px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
cursor: pointer;
transition: opacity 0.15s, box-shadow 0.15s;
}
.timeline-detail-bar:hover {
opacity: 0.85;
z-index: 10;
box-shadow: 0 0 6px rgba(255,255,255,0.3);
}
.covered-bar {
background-image: repeating-linear-gradient(
45deg, transparent, transparent 3px,
rgba(255,255,255,0.18) 3px, rgba(255,255,255,0.18) 6px
) !important;
opacity: 0.55 !important;
}
.covered-badge {
font-size: 10px;
background: rgba(255,120,100,0.25);
color: #ff7864;
border: 1px solid rgba(255,120,100,0.45);
border-radius: 3px;
padding: 0 4px;
margin-left: 5px;
vertical-align: middle;
white-space: nowrap;
}
</style>
'''
JS_TEMPLATE = """
<script>
let currentTooltip = null;
const tooltipData = @@TOOLTIP_JSON@@;
const defaultFields = @@FIELDS_JSON@@;
const tlData = @@TL_JSON@@;
function showTooltip(el) {
const idx = el.dataset.index;
if (currentTooltip && currentTooltip.classList.contains('visible') &&
currentTooltip.dataset.currentIdx === idx) {
hideTooltip();
return;
}
if (!tooltipData[idx]) return;
if (!currentTooltip) {
currentTooltip = document.createElement('div');
currentTooltip.className = 'kernel-tooltip';
document.body.appendChild(currentTooltip);
}
currentTooltip.innerHTML = tooltipData[idx];
currentTooltip.classList.add('visible');
currentTooltip.dataset.currentIdx = idx;
currentTooltip.style.left = '';
currentTooltip.style.top = '';
currentTooltip.style.right = '';
const rect = el.getBoundingClientRect();
currentTooltip.style.position = 'fixed';
const tooltipEl = currentTooltip;
const tw = tooltipEl.offsetWidth;
const th = tooltipEl.offsetHeight;
const vw = window.innerWidth;
const vh = window.innerHeight;
let left = rect.right + 12;
let top = rect.top;
if (left + tw > vw - 10) {
left = Math.max(10, vw - tw - 10);
}
if (top + th > vh - 10) {
top = Math.max(10, vh - th - 10);
}
if (top < 10) top = 10;
currentTooltip.style.left = left + 'px';
currentTooltip.style.top = top + 'px';
}
function hideTooltip() {
if (currentTooltip) {
currentTooltip.classList.remove('visible');
}
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('report-theme', theme);
document.getElementById('theme-select').value = theme;
}
function initTheme() {
const saved = localStorage.getItem('report-theme') || '@@DEFAULT_THEME@@';
applyTheme(saved);
}
function toggleKernels(visible) {
document.querySelectorAll('.tree-node[data-type="kernel"]').forEach(node => {
if (visible) {
node.classList.remove('hidden-kernel');
} else {
node.classList.add('hidden-kernel');
}
});
if (visible) {
document.querySelectorAll('.tree-node[data-type="kernel"]').forEach(kernel => {
let parent = kernel.parentElement;
while (parent) {
if (parent.classList.contains('tree-node') && parent.classList.contains('collapsed')) {
parent.classList.remove('collapsed');
parent.classList.add('expanded');
}
parent = parent.parentElement;
}
});
}
}
function updateKernelMetaFields() {
const panel = document.getElementById('kernel-fields-panel');
const checked = Array.from(panel.querySelectorAll('input:checked')).map(cb => cb.value);
document.querySelectorAll('.kernel-meta').forEach(meta => {
meta.querySelectorAll('.kernel-meta-item').forEach(item => {
const field = item.dataset.field;
item.style.display = checked.includes(field) ? 'flex' : 'none';
});
});
}
/* ========== Timeline Logic ========== */
let tlTooltip = null;
let currentExpandedNode = null;
let navHistory = [];
function formatUs(us) {
if (us >= 1000) return (us / 1000).toFixed(1) + 'ms';
return us.toFixed(1) + 'us';
}
function initTimelineTooltip() {
if (!tlTooltip) {
tlTooltip = document.createElement('div');
tlTooltip.className = 'timeline-tooltip';
document.body.appendChild(tlTooltip);
}
}
function showTimelineTooltip(el, barInfo) {
initTimelineTooltip();
let streamInfo = '';
if (barInfo.streams && barInfo.streams.length > 0) {
streamInfo = '<div class="timeline-tooltip-streams">' +
'<span class="timeline-tooltip-label">Streams:</span> ' +
barInfo.streams.map(s => 'Stream ' + s).join(', ') + '</div>';
}
const countSuffix = barInfo.multiplier > 1 ? ' (×' + barInfo.multiplier + ')' : '';
const hasChildrenHint = barInfo.has_children ? '点击展开子节点' : '点击查看算子详情';
tlTooltip.innerHTML =
'<div class="timeline-tooltip-title">' + barInfo.name + countSuffix + '</div>' +
'<div class="timeline-tooltip-row"><span class="timeline-tooltip-label">时间:</span> ' +
formatUs(barInfo.duration) + '</div>' +
'<div class="timeline-tooltip-row"><span class="timeline-tooltip-label">Kernels:</span> ' +
barInfo.kernel_count + '</div>' +
streamInfo +
'<div style="margin-top:6px;color:var(--text-secondary);font-size:11px">' + hasChildrenHint + '</div>';
tlTooltip.classList.add('visible');
const rect = el.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
let left = rect.right + 10;
let top = rect.top;
if (left + 320 > vw) left = Math.max(10, rect.left - 330);
if (top + 180 > vh) top = Math.max(10, vh - 190);
if (top < 10) top = 10;
tlTooltip.style.left = left + 'px';
tlTooltip.style.top = top + 'px';
}
function hideTimelineTooltip() {
if (tlTooltip) tlTooltip.classList.remove('visible');
}
function showOpsDetail(nodeIndex) {
if (!tlData.bar_data || !tlData.bar_data[nodeIndex]) return;
const bar = tlData.bar_data[nodeIndex];
const panel = document.getElementById('timeline-detail-panel');
const title = document.getElementById('timeline-detail-title');
const opsContainer = document.getElementById('timeline-detail-ops');
const countSuffix = bar.multiplier > 1 ? ' (×' + bar.multiplier + ')' : '';
title.textContent = bar.name + countSuffix + ' - 算子列表';
const allOps = [];
if (bar.per_stream_ops) {
for (const sid in bar.per_stream_ops) {
for (const op of bar.per_stream_ops[sid]) {
allOps.push(op);
}
}
}
if (allOps.length === 0) {
opsContainer.innerHTML = '<div style="padding:10px;color:var(--text-secondary)">无算子数据</div>';
panel.classList.add('visible');
return;
}
const timeMin = Math.min(...allOps.map(o => o.start));
const timeMax = Math.max(...allOps.map(o => o.end));
const timeDuration = Math.max(1, timeMax - timeMin);
const streamColors = {};
const palette = ['#4fc1ff', '#6a9955', '#ce9178', '#c586c0', '#569cd6', '#dcdcaa', '#e06c75'];
let ci = 0;
const sortedSids = Object.keys(bar.per_stream_ops).sort((a, b) => Number(a) - Number(b));
for (const sid of sortedSids) {
streamColors[sid] = palette[ci % palette.length];
ci++;
}
// Determine dominant stream (most ops)
const dominantSid = sortedSids.reduce((a, b) =>
(bar.per_stream_ops[a].length >= bar.per_stream_ops[b].length ? a : b), sortedSids[0]);
const dominantOps = (bar.per_stream_ops[dominantSid] || []).map(o => ({
start: o.start, end: o.end
}));
function isCoveredByDominant(op) {
return dominantOps.some(d => d.start < op.end && d.end > op.start);
}
let html = '';
for (const sid of sortedSids) {
const ops = bar.per_stream_ops[sid].sort((a, b) => a.start - b.start);
const color = streamColors[sid];
const isAux = sid !== dominantSid;
const coveredCount = isAux ? ops.filter(op => isCoveredByDominant(op)).length : 0;
const coveredBadge = (isAux && coveredCount > 0)
? '<span class="covered-badge">' + coveredCount + ' covered</span>' : '';
html += '<div class="timeline-stream-row">';
html += '<span class="timeline-stream-label" style="background:' + color +
'22;color:' + color + ';border:1px solid ' + color + '44">Stream ' + sid +
' (' + ops.length + ')' + coveredBadge + '</span>';
html += '<div class="timeline-stream-ops">';
ops.forEach((op, opIndex) => {
const leftPct = ((op.start - timeMin) / timeDuration) * 100;
const widthPct = Math.max(1, (op.duration / timeDuration) * 100);
const shortName = op.name.length > 10 ? op.name.substring(0, 8) + '..' : op.name;
const covered = isAux && isCoveredByDominant(op);
const coveredTip = covered ? ('\\n⚠ covered by stream ' + dominantSid) : '';
const tipText = op.name + '\\n历时: ' + formatUs(op.duration) +
'\\nStart: ' + formatUs(op.start) + coveredTip;
const showText = widthPct > 6;
// Alternating colors: even = full opacity, odd = semi-transparent + top border
const isEven = opIndex % 2 === 0;
const barAlpha = isEven ? 'ee' : '88';
const borderExtra = isEven ? '' : ';border-top:2px solid ' + color + 'cc';
const coveredClass = covered ? ' covered-bar' : '';
html += '<span class="timeline-detail-bar' + coveredClass + '" style="left:' +
leftPct.toFixed(1) + '%;width:' + widthPct.toFixed(1) + '%;background:' +
color + barAlpha + borderExtra + '" title="' + tipText + '">' +
(showText ? shortName : '') + '</span>';
});
html += '</div></div>';
}
opsContainer.innerHTML = html;
panel.classList.add('visible');
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function renderGantt(parentNode, children) {
const container = document.getElementById('gantt-container');
if (!container || !children || children.length === 0) {
container.innerHTML = '<div style="padding:10px;color:var(--text-secondary)">无子节点数据</div>';
return;
}
const parentStart = parentNode.start;
const parentDuration = parentNode.duration || 1;
const allStreams = new Set();
children.forEach(c => c.streams.forEach(s => allStreams.add(s)));
const sortedStreams = Array.from(allStreams).sort();
let html = '';
sortedStreams.forEach(stream => {
const barsOnStream = children.filter(c => c.streams.includes(stream));
if (barsOnStream.length === 0) return;
html += '<div class="gantt-stream-group">';
html += '<div class="gantt-stream-row">';
html += '<span class="gantt-stream-label">Stream ' + stream + '</span>';
html += '<div class="gantt-bars">';
barsOnStream.forEach(child => {
const leftPct = Math.max(0, (child.start - parentStart) / parentDuration * 100);
const widthPct = Math.max(1.5, child.duration / parentDuration * 100);
const rightPct = Math.min(100, leftPct + widthPct);
const actualWidth = rightPct - leftPct;
const centerPct = leftPct + actualWidth / 2;
const isDominant = child.dominant_stream === stream;
const opacity = isDominant ? '1' : '0.4';
const borderStyle = isDominant ? '' : ';border:1px dashed ' + child.color;
html += '<div class="gantt-bar' + (isDominant ? '' : ' gantt-bar-secondary') +
'" style="left:' + leftPct.toFixed(1) + '%;width:' + actualWidth.toFixed(1) +
'%;background:' + child.color + ';opacity:' + opacity + borderStyle +
'" data-node-index="' + child.node_index + '" data-center="' + centerPct.toFixed(1) +
'" data-color="' + child.color + '">' +
(isDominant ? '<div class="gantt-bar-arrow" style="border-top-color:' +
child.color + '"></div>' : '') +
'</div>';
});
html += '</div></div>';
html += '<div class="gantt-labels-row">';
barsOnStream.forEach(child => {
const leftPct = Math.max(0, (child.start - parentStart) / parentDuration * 100);
const widthPct = Math.max(1.5, child.duration / parentDuration * 100);
const centerPct = Math.min(98, Math.max(2, leftPct + widthPct / 2));
const isDominant = child.dominant_stream === stream;
const shortName = child.name.length > 12 ? child.name.substring(0, 10) + '..' : child.name;
if (!isDominant) return;
html += '<div class="gantt-label" style="left:' + centerPct.toFixed(1) +
'%;color:' + child.color + ';border-color:' + child.color +
'66" data-center="' + centerPct.toFixed(1) + '">' +
shortName + '</div>';
});
html += '</div></div>';
});
const timeMarkers = [0, 0.25, 0.5, 0.75, 1].map(r => formatUs(parentStart + parentDuration * r));
html += '<div class="gantt-time-axis">' +
'<span>' + timeMarkers[0] + '</span>' +
'<span>' + timeMarkers[1] + '</span>' +
'<span>' + timeMarkers[2] + '</span>' +
'<span>' + timeMarkers[3] + '</span>' +
'<span>' + timeMarkers[4] + '</span></div>';
container.innerHTML = html;
layoutLabels(container);
container.querySelectorAll('.gantt-bar').forEach(bar => {
const ni = parseInt(bar.dataset.nodeIndex);
const info = tlData.bar_data ? tlData.bar_data[ni] : null;
if (!info) return;
bar.addEventListener('mouseenter', () => showTimelineTooltip(bar, info));
bar.addEventListener('mouseleave', () => setTimeout(() => {
if (!tlTooltip || !tlTooltip.matches(':hover')) hideTimelineTooltip(); }, 150));
bar.addEventListener('click', (e) => { e.stopPropagation(); hideTimelineTooltip(); showOpsDetail(ni); });
});
}
function layoutLabels(container) {
container.querySelectorAll('.gantt-labels-row').forEach(row => {
const labels = Array.from(row.querySelectorAll('.gantt-label'));
if (labels.length === 0) return;
labels.sort((a, b) => parseFloat(a.dataset.center) - parseFloat(b.dataset.center));
const minGap = 10;
for (let i = 1; i < labels.length; i++) {
const prev = labels[i - 1];
const curr = labels[i];
const prevLeft = parseFloat(prev.style.left);
let currLeft = parseFloat(curr.style.left);
const prevWidth = prev.offsetWidth;
const currWidth = curr.offsetWidth;
const threshold = (prevWidth + currWidth) / 2 / row.offsetWidth * 100 + minGap;
if (currLeft - prevLeft < threshold) {
currLeft = prevLeft + threshold;
if (currLeft > 98) currLeft = 98;
curr.style.left = currLeft.toFixed(1) + '%';
}
}
});
}
function renderTree(parentNode, children) {
const container = document.getElementById('tree-content');
if (!container || !children || children.length === 0) {
container.innerHTML = '<div style="padding:10px;color:var(--text-secondary)">无子节点</div>';
return;
}
const baseDepth = parentNode.depth;
let html = '';
function renderNode(node, depth) {
const indent = (depth - baseDepth) * 20;
const hasChildren = node.has_children;
const icon = hasChildren ? '▶' : '─';
const clickableClass = hasChildren ? 'clickable' : '';
const streamsStr = node.streams.slice(0, 3).join(',') + (node.streams.length > 3 ? '..' : '');
html += '<div class="tree-item ' + clickableClass + '" data-node-index="' +
node.node_index + '" style="padding-left:' + indent + 'px">';
html += '<span class="tree-icon">' + icon + '</span>';
html += '<span class="tree-name">' + node.name + '</span>';
html += '<span class="tree-streams">[' + streamsStr + ']</span>';
html += '<span class="tree-duration">' + formatUs(node.duration) + '</span>';
html += '</div>';
}
children.forEach(child => renderNode(child, child.depth));
container.innerHTML = html;
container.querySelectorAll('.tree-item.clickable').forEach(item => {
const ni = parseInt(item.dataset.nodeIndex);
item.addEventListener('click', (e) => {
e.stopPropagation();
showNodeChildren(ni);
});
item.addEventListener('mouseenter', () => {
const info = tlData.bar_data ? tlData.bar_data[ni] : null;
if (info) showTimelineTooltip(item, info);
});
item.addEventListener('mouseleave', () => setTimeout(() => {
if (!tlTooltip || !tlTooltip.matches(':hover')) hideTimelineTooltip(); }, 150));
});
container.querySelectorAll('.tree-item:not(.clickable)').forEach(item => {
const ni = parseInt(item.dataset.nodeIndex);
item.addEventListener('click', (e) => {
e.stopPropagation();
showOpsDetail(ni);
});
});
}
function showNodeChildren(nodeIndex, addToHistory = true) {
if (!tlData.bar_data || !tlData.bar_data[nodeIndex]) return;
const node = tlData.bar_data[nodeIndex];
const childrenIndices = node.children_indices || [];
if (childrenIndices.length === 0) {
showOpsDetail(nodeIndex);
return;
}
if (addToHistory && currentExpandedNode !== null && currentExpandedNode !== nodeIndex) {
navHistory.push(currentExpandedNode);
}
const children = childrenIndices.map(i => tlData.bar_data[i]).filter(c => c);
const expandArea = document.getElementById('timeline-expand-area');
const title = document.getElementById('expand-title');
const countSuffix = node.multiplier > 1 ? ' (×' + node.multiplier + ')' : '';
title.textContent = node.name + countSuffix + ' - 子节点多流时序';
renderGantt(node, children);
renderTree(node, children);
renderBreadcrumb(nodeIndex);
currentExpandedNode = nodeIndex;
expandArea.style.display = 'block';
expandArea.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function renderBreadcrumb(currentNodeIndex) {
const bcContainer = document.getElementById('expand-breadcrumb');
if (!bcContainer) return;
const pathIndices = [];
let idx = currentNodeIndex;
while (idx >= 0 && tlData.bar_data[idx]) {
pathIndices.unshift(idx);
idx = tlData.bar_data[idx].parent_index;
}
let html = '';
pathIndices.forEach((ni, i) => {
const info = tlData.bar_data[ni];
const isCurrent = (ni === currentNodeIndex);
const countSuffix = info.multiplier > 1 ? ' ×' + info.multiplier : '';
const label = info.name + countSuffix;
if (i > 0) {
html += '<span class="breadcrumb-sep">›</span>';
}
html += '<span class="breadcrumb-item' + (isCurrent ? ' current' : '') +
'" data-node-index="' + ni + '">' + label + '</span>';
});
bcContainer.innerHTML = html;
bcContainer.querySelectorAll('.breadcrumb-item:not(.current)').forEach(item => {
const ni = parseInt(item.dataset.nodeIndex);
item.addEventListener('click', (e) => {
e.stopPropagation();
const targetIdx = navHistory.indexOf(ni);
if (targetIdx >= 0) {
navHistory = navHistory.slice(0, targetIdx);
}
showNodeChildren(ni, false);
});
});
}
function goBackToParent() {
if (navHistory.length === 0) {
hideExpandArea();
return;
}
const prevNodeIndex = navHistory.pop();
showNodeChildren(prevNodeIndex, false);
}
function hideExpandArea() {
const expandArea = document.getElementById('timeline-expand-area');
if (expandArea) {
expandArea.style.display = 'none';
currentExpandedNode = null;
navHistory = [];
}
}
document.addEventListener('DOMContentLoaded', function() {
let kernelsVisible = true;
const kernelBtn = document.getElementById('toggle-kernels-btn');
kernelBtn.addEventListener('click', () => {
kernelsVisible = !kernelsVisible;
toggleKernels(kernelsVisible);
kernelBtn.textContent = kernelsVisible ?
'\u9690\u85cfKernel\u5e8f\u5217' : '\u663e\u793aKernel\u5e8f\u5217';
});
let kernelMetaVisible = true;
const kernelMetaBtn = document.getElementById('toggle-kernel-meta-btn');
kernelMetaBtn.addEventListener('click', () => {
kernelMetaVisible = !kernelMetaVisible;
document.querySelectorAll('.kernel-meta').forEach(el => {
el.style.display = kernelMetaVisible ? 'grid' : 'none';
});
kernelMetaBtn.textContent = kernelMetaVisible ?
'\u9690\u85cfKernel\u4fe1\u606f' : '\u663e\u793aKernel\u4fe1\u606f';
});
let allExpanded = false;
const expandBtn = document.getElementById('toggle-expand-btn');
const depthSelect = document.getElementById('depth-select');
function applyDepthExpansion(maxDepth) {
document.querySelectorAll('.tree-node').forEach(node => {
const depth = parseInt(node.dataset.depth);
if (!node.classList.contains('leaf') && !node.classList.contains('kernel-only')) {
if (depth < maxDepth) {
node.classList.remove('collapsed');
node.classList.add('expanded');
} else {
node.classList.remove('expanded');
node.classList.add('collapsed');
}
}
});
}
expandBtn.addEventListener('click', () => {
allExpanded = !allExpanded;
if (allExpanded) {
applyDepthExpansion(parseInt(depthSelect.value));
expandBtn.textContent = '\u5168\u90e8\u6536\u8d77';
} else {
document.querySelectorAll('.tree-node:not(.leaf)').forEach(node => {
node.classList.remove('expanded');
node.classList.add('collapsed');
});
expandBtn.textContent = '\u5168\u90e8\u5c55\u5f00';
}
});
depthSelect.addEventListener('change', () => {
applyDepthExpansion(parseInt(depthSelect.value));
if (allExpanded) {
expandBtn.textContent = '\u5168\u90e8\u6536\u8d77';
}
});
let auxiliaryVisible = false;
const auxiliaryBtn = document.getElementById('toggle-auxiliary-btn');
document.querySelectorAll('.tree-node[data-category="auxiliary"]').forEach(node => {
node.style.display = 'none';
});
document.querySelectorAll('.timeline-node-item[data-category="auxiliary"]').forEach(node => {
node.style.display = 'none';
});
auxiliaryBtn.addEventListener('click', () => {
auxiliaryVisible = !auxiliaryVisible;
document.querySelectorAll('.tree-node[data-category="auxiliary"]').forEach(node => {
node.style.display = auxiliaryVisible ? '' : 'none';
});
document.querySelectorAll('.timeline-node-item[data-category="auxiliary"]').forEach(node => {
node.style.display = auxiliaryVisible ? '' : 'none';
});
auxiliaryBtn.textContent = auxiliaryVisible ?
'\u9690\u85cf\u8f85\u52a9\u5c42\u6b21' : '\u663e\u793a\u8f85\u52a9\u5c42\u6b21';
});
const fieldsPanel = document.getElementById('kernel-fields-panel');
const fieldsBtn = document.getElementById('kernel-fields-btn');
fieldsPanel.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = defaultFields.includes(cb.value);
});
fieldsBtn.addEventListener('click', (e) => {
e.stopPropagation();
fieldsPanel.classList.toggle('visible');
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.kernel-fields-config')) {
fieldsPanel.classList.remove('visible');
}
});
fieldsPanel.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', updateKernelMetaFields);
});
document.getElementById('fields-select-all').addEventListener('click', (e) => {
e.stopPropagation();
fieldsPanel.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true);
updateKernelMetaFields();
});
document.getElementById('fields-select-none').addEventListener('click', (e) => {
e.stopPropagation();
fieldsPanel.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
updateKernelMetaFields();
});
document.getElementById('fields-reset').addEventListener('click', (e) => {
e.stopPropagation();
fieldsPanel.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = defaultFields.includes(cb.value);
});
updateKernelMetaFields();
});
let semanticAllExpanded = false;
const semanticBtn = document.getElementById('toggle-semantic-btn');
semanticBtn.addEventListener('click', () => {
semanticAllExpanded = !semanticAllExpanded;
document.querySelectorAll('.node-semantic-wrapper').forEach(w => {
if (semanticAllExpanded) {
w.classList.add('expanded');
} else {
const full = w.dataset.full || '';
if (full.length > 120) {
w.classList.remove('expanded');
}
}
});
semanticBtn.textContent = semanticAllExpanded ?
'\u6536\u8d77\u5168\u90e8\u8bed\u4e49' : '\u5c55\u5f00\u5168\u90e8\u8bed\u4e49';
});
document.getElementById('theme-select').addEventListener('change', function() {
applyTheme(this.value);
});
document.addEventListener('click', function(e) {
const btn = e.target.closest('.semantic-expand-btn');
if (!btn) return;
e.stopPropagation();
const wrapper = btn.closest('.node-semantic-wrapper');
if (wrapper) {
wrapper.classList.toggle('expanded');
}
});
document.querySelectorAll('.tree-node:not(.leaf)').forEach(node => {
const header = node.querySelector('.node-header');
header.addEventListener('click', (e) => {
if (e.target.closest('[data-type="kernel"]')) return;
if (e.target.closest('.semantic-expand-btn')) return;
if (e.target.closest('.node-semantic-wrapper')) return;
if (e.target.closest('.kernel-info-btn')) return;
node.classList.toggle('collapsed');
node.classList.toggle('expanded');
});
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.tree-node[data-type="kernel"]') &&
!e.target.closest('.kernel-tooltip') &&
!e.target.closest('.kernel-info-btn')) {
hideTooltip();
}
});
document.querySelectorAll('.kernel-info-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
showTooltip(btn);
});
});
if (currentTooltip) {
currentTooltip.addEventListener('mouseleave', hideTooltip);
}
initTheme();
/* ========== Timeline Event Listeners ========== */
document.querySelectorAll('.timeline-node-item').forEach(item => {
const ni = parseInt(item.dataset.nodeIndex);
const info = tlData.bar_data ? tlData.bar_data[ni] : null;
if (!info) return;
item.addEventListener('mouseenter', () => showTimelineTooltip(item, info));
item.addEventListener('mouseleave', () => setTimeout(() => {
if (!tlTooltip || !tlTooltip.matches(':hover')) hideTimelineTooltip(); }, 150));
item.addEventListener('click', (e) => {
e.stopPropagation();
hideTimelineTooltip();
if (info.has_children) {
showNodeChildren(ni);
} else {
showOpsDetail(ni);
}
});
});
if (tlTooltip) {
tlTooltip.addEventListener('mouseleave', hideTimelineTooltip);
}
const expandCloseBtn = document.getElementById('expand-close-btn');
if (expandCloseBtn) {
expandCloseBtn.addEventListener('click', hideExpandArea);
}
const detailCloseBtn = document.getElementById('timeline-detail-close');
if (detailCloseBtn) {
detailCloseBtn.addEventListener('click', () => {
document.getElementById('timeline-detail-panel').classList.remove('visible');
});
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.timeline-node-item') && !e.target.closest('.timeline-tooltip') &&
!e.target.closest('.timeline-expand-area') && !e.target.closest('.timeline-detail-panel')) {
hideTimelineTooltip();
}
});
});
</script>
"""