|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>uvnote Integration Test Report</title> |
|
|
<script> |
|
|
|
|
|
(function() { |
|
|
const configTheme = 'light'; |
|
|
const hasConfigUi = false; |
|
|
const configUi = hasConfigUi ? 'None' : null; |
|
|
const hasWidgetsConfig = false; |
|
|
const widgetsOn = hasWidgetsConfig ? false : true; |
|
|
let theme; |
|
|
if (configTheme === 'auto') { |
|
|
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; |
|
|
} else { |
|
|
theme = localStorage.getItem('uvnote-theme') || configTheme; |
|
|
} |
|
|
document.documentElement.setAttribute('data-theme', theme); |
|
|
|
|
|
|
|
|
let ui = hasConfigUi ? configUi : (localStorage.getItem('uvnote-ui') || 'default'); |
|
|
if (ui !== 'default' && ui !== 'none' && ui !== 'monocolor') { ui = 'default'; } |
|
|
document.documentElement.setAttribute('data-ui', ui); |
|
|
|
|
|
|
|
|
document.documentElement.setAttribute('data-widgets', widgetsOn ? 'on' : 'off'); |
|
|
})(); |
|
|
</script> |
|
|
<style> |
|
|
:root[data-theme="light"] { |
|
|
--bg-primary: #ffffff; |
|
|
--bg-secondary: #f6f8fa; |
|
|
--bg-tertiary: #f8f9fa; |
|
|
--bg-code: #f8f9fa; |
|
|
--bg-error: #fdf2f2; |
|
|
--bg-artifact: #e6f3ff; |
|
|
--bg-artifact-hover: #d0e7ff; |
|
|
|
|
|
--text-primary: #333; |
|
|
--text-secondary: #656d76; |
|
|
--text-error: #c53030; |
|
|
--text-link: #0969da; |
|
|
|
|
|
--border-primary: #e1e5e9; |
|
|
--border-error: #e53e3e; |
|
|
--border-cell-failed: #d73a49; |
|
|
|
|
|
--shadow: rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
|
|
|
:root[data-theme="dark"] { |
|
|
--bg-primary: #0a0a0a; |
|
|
--bg-secondary: #121212; |
|
|
--bg-tertiary: #181818; |
|
|
--bg-code: #0d0d0d; |
|
|
--bg-error: #1a0f0f; |
|
|
--bg-artifact: #151515; |
|
|
--bg-artifact-hover: #1a1a1a; |
|
|
|
|
|
--text-primary: #e0e0e0; |
|
|
--text-secondary: #888888; |
|
|
--text-error: #ff6b6b; |
|
|
--text-link: #64b5f6; |
|
|
|
|
|
--border-primary: #2a2a2a; |
|
|
--border-error: #ff6b6b; |
|
|
--border-cell-failed: #ff6b6b; |
|
|
|
|
|
--shadow: rgba(255, 255, 255, 0.05); |
|
|
} |
|
|
|
|
|
:root[data-ui="monocolor"] { --mono-color: #0a66ff; } |
|
|
:root[data-ui="monocolor"][data-theme="light"] { |
|
|
--bg-primary: #ffffff; |
|
|
} |
|
|
:root[data-ui="monocolor"][data-theme="dark"] { |
|
|
--bg-primary: #000000; |
|
|
} |
|
|
:root[data-ui="monocolor"] { |
|
|
--bg-secondary: var(--bg-primary); |
|
|
--bg-tertiary: var(--bg-primary); |
|
|
--bg-code: var(--bg-primary); |
|
|
--bg-error: var(--bg-primary); |
|
|
--bg-artifact: var(--bg-primary); |
|
|
--bg-artifact-hover: var(--bg-primary); |
|
|
|
|
|
--text-primary: var(--mono-color); |
|
|
--text-secondary: var(--mono-color); |
|
|
--text-error: var(--mono-color); |
|
|
--text-link: var(--mono-color); |
|
|
|
|
|
--border-primary: var(--mono-color); |
|
|
--border-error: var(--mono-color); |
|
|
--border-cell-failed: var(--mono-color); |
|
|
|
|
|
--shadow: none; |
|
|
} |
|
|
:root[data-ui="monocolor"] a { color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .menu-button, |
|
|
:root[data-ui="monocolor"] .theme-toggle, |
|
|
:root[data-ui="monocolor"] .reset-toggle { background: var(--bg-primary); color: var(--mono-color); border-color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .menu-button:hover, |
|
|
:root[data-ui="monocolor"] .theme-toggle:hover, |
|
|
:root[data-ui="monocolor"] .reset-toggle:hover { background: var(--bg-primary); color: var(--mono-color); border-color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .menu-dropdown { background: var(--bg-primary); border-color: var(--mono-color); box-shadow: none; } |
|
|
:root[data-ui="monocolor"] .menu-item { color: var(--mono-color); border-bottom-color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .system-info { background: var(--bg-primary); border-color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .cell { border-color: var(--mono-color); background: var(--bg-primary); } |
|
|
:root[data-ui="monocolor"] .cell-header { background: var(--bg-primary); border-bottom-color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .artifact { background: var(--bg-primary); border-color: var(--mono-color); color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .artifact:hover { background: var(--bg-primary); } |
|
|
:root[data-ui="monocolor"] .artifact-preview img, |
|
|
:root[data-ui="monocolor"] .artifact-preview svg { border-color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .status-widget { background: var(--bg-primary); border-color: var(--mono-color); color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .minimap, |
|
|
:root[data-ui="monocolor"] .file-explorer, |
|
|
:root[data-ui="monocolor"] .tools-widget { |
|
|
background: var(--bg-primary); |
|
|
border-color: var(--mono-color); |
|
|
color: var(--mono-color); |
|
|
} |
|
|
:root[data-ui="monocolor"] .cell-code { |
|
|
background: var(--bg-primary); |
|
|
border-bottom-color: var(--mono-color); |
|
|
} |
|
|
:root[data-ui="monocolor"] .tools-title, |
|
|
:root[data-ui="monocolor"] .file-explorer-section-title, |
|
|
:root[data-ui="monocolor"] .minimap-title { color: var(--mono-color); border-bottom-color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .tool-button { background: var(--bg-primary); border-color: var(--mono-color); color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .tool-button.active { border-color: var(--mono-color); } |
|
|
:root[data-ui="monocolor"] .file-explorer-item, |
|
|
:root[data-ui="monocolor"] .minimap-item { color: var(--mono-color); } |
|
|
|
|
|
:root[data-ui="monocolor"] .highlight { background: var(--bg-primary) !important; color: var(--mono-color) !important; } |
|
|
:root[data-ui="monocolor"] .highlight *, |
|
|
:root[data-ui="monocolor"] .highlight .hll { color: var(--mono-color) !important; background: transparent !important; border-color: var(--mono-color) !important; } |
|
|
|
|
|
:root { --code-font-size: 0.95rem; --code-line-height: 1.5; --code-pad-y: 0.75rem; } |
|
|
|
|
|
:root[data-ui="none"] { |
|
|
--bg-primary: #ffffff; |
|
|
--bg-secondary: transparent; |
|
|
--bg-tertiary: transparent; |
|
|
--bg-code: #f9f9f9; |
|
|
--bg-error: #fff0f0; |
|
|
--bg-artifact: #f0f7ff; |
|
|
--bg-artifact-hover: #e5f1ff; |
|
|
|
|
|
--text-primary: #000000; |
|
|
--text-secondary: #222222; |
|
|
--text-error: #a00000; |
|
|
--text-link: #0000ee; |
|
|
|
|
|
--border-primary: #cccccc; |
|
|
--border-error: #cc0000; |
|
|
--border-cell-failed: #cc0000; |
|
|
|
|
|
--shadow: none; |
|
|
} |
|
|
html { |
|
|
overscroll-behavior: none; |
|
|
} |
|
|
body { |
|
|
font-family: 'Cascadia Mono', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, 'Consolas', monospace; |
|
|
line-height: 1.4; |
|
|
max-width: 1000px; |
|
|
margin: 0 auto; |
|
|
padding: 15px; |
|
|
color: var(--text-primary); |
|
|
background: var(--bg-primary); |
|
|
transition: background-color 0.2s ease, color 0.2s ease; |
|
|
overscroll-behavior: none; |
|
|
} |
|
|
|
|
|
:root[data-ui="none"] body { |
|
|
font-family: 'Times New Roman', Times, serif; |
|
|
line-height: 1.5; |
|
|
max-width: 860px; |
|
|
padding: 12px; |
|
|
background: #ffffff; |
|
|
color: #000000; |
|
|
transition: none; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.controls { |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: flex-end; |
|
|
gap: 0.25rem; |
|
|
z-index: 1000; |
|
|
} |
|
|
.controls-buttons { display: flex; gap: 0.5rem; } |
|
|
|
|
|
.menu-button { |
|
|
position: relative; |
|
|
background: var(--bg-secondary); |
|
|
border: 1px solid var(--border-primary); |
|
|
padding: 8px 12px; |
|
|
border-radius: 2px; |
|
|
color: var(--text-secondary); |
|
|
cursor: pointer; |
|
|
font-family: inherit; |
|
|
font-size: 0.9rem; |
|
|
user-select: none; |
|
|
} |
|
|
|
|
|
:root[data-ui="none"][data-widgets="on"] .menu-button, |
|
|
:root[data-ui="none"][data-widgets="on"] .theme-toggle, |
|
|
:root[data-ui="none"][data-widgets="on"] .reset-toggle { |
|
|
background: #f6f6f6; |
|
|
border: 1px solid #cccccc; |
|
|
color: #222222; |
|
|
} |
|
|
|
|
|
.menu-button:hover { |
|
|
color: var(--text-primary); |
|
|
background: var(--bg-tertiary); |
|
|
} |
|
|
|
|
|
|
|
|
.status-widget { |
|
|
position: fixed; |
|
|
right: 20px; |
|
|
bottom: 20px; |
|
|
width: auto; |
|
|
max-width: 260px; |
|
|
background: var(--bg-secondary); |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 2px; |
|
|
padding: 6px 8px; |
|
|
font-size: 0.8rem; |
|
|
color: var(--text-secondary); |
|
|
z-index: 100; |
|
|
} |
|
|
.status-widget strong { color: var(--text-primary); } |
|
|
:root[data-ui="none"][data-widgets="on"] .status-widget { background: #f6f6f6; border-color: #ccc; color: #222; } |
|
|
:root[data-ui="none"][data-widgets="on"] .menu-button:hover, |
|
|
:root[data-ui="none"][data-widgets="on"] .theme-toggle:hover, |
|
|
:root[data-ui="none"][data-widgets="on"] .reset-toggle:hover { |
|
|
background: #ededed; |
|
|
border-color: #bbbbbb; |
|
|
color: #000000; |
|
|
} |
|
|
|
|
|
.menu-dropdown { |
|
|
position: absolute; |
|
|
top: 100%; |
|
|
right: 0; |
|
|
background: var(--bg-secondary); |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 4px; |
|
|
box-shadow: 0 4px 12px var(--shadow); |
|
|
min-width: 160px; |
|
|
opacity: 0; |
|
|
visibility: hidden; |
|
|
transform: translateY(-8px); |
|
|
transition: all 0.2s ease; |
|
|
z-index: 1001; |
|
|
margin-top: 4px; |
|
|
} |
|
|
:root[data-ui="none"][data-widgets="on"] .menu-dropdown { background: #ffffff; border: 1px solid #cccccc; box-shadow: none; } |
|
|
|
|
|
.menu-button.active .menu-dropdown { |
|
|
opacity: 1; |
|
|
visibility: visible; |
|
|
transform: translateY(0); |
|
|
} |
|
|
|
|
|
.menu-item { |
|
|
display: block; |
|
|
padding: 8px 12px; |
|
|
color: var(--text-secondary); |
|
|
text-decoration: none; |
|
|
font-size: 0.85rem; |
|
|
border-bottom: 1px solid var(--border-primary); |
|
|
cursor: pointer; |
|
|
} |
|
|
:root[data-ui="none"] .menu-item { color: #000; border-bottom: 1px solid #eee; } |
|
|
|
|
|
.menu-item:last-child { |
|
|
border-bottom: none; |
|
|
} |
|
|
|
|
|
.menu-item:hover { |
|
|
background: var(--bg-tertiary); |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.menu-checkbox { |
|
|
display: inline-block; |
|
|
width: 16px; |
|
|
font-family: monospace; |
|
|
color: var(--text-link); |
|
|
} |
|
|
|
|
|
.theme-toggle, |
|
|
.reset-toggle { |
|
|
background: var(--bg-secondary); |
|
|
border: 1px solid var(--border-primary); |
|
|
padding: 8px 12px; |
|
|
border-radius: 4px; |
|
|
color: var(--text-secondary); |
|
|
cursor: pointer; |
|
|
font-family: inherit; |
|
|
font-size: 0.9rem; |
|
|
user-select: none; |
|
|
} |
|
|
|
|
|
.theme-toggle:hover, |
|
|
.reset-toggle:hover { |
|
|
color: var(--text-primary); |
|
|
background: var(--bg-tertiary); |
|
|
} |
|
|
|
|
|
.system-info { |
|
|
background: var(--bg-secondary); |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 4px; |
|
|
padding: 8px 12px; |
|
|
margin-bottom: 16px; |
|
|
font-size: 0.85em; |
|
|
color: var(--text-secondary); |
|
|
} |
|
|
|
|
|
.system-info-header { |
|
|
font-weight: 600; |
|
|
color: var(--text-primary); |
|
|
margin-bottom: 2px; |
|
|
} |
|
|
|
|
|
.system-info-content { |
|
|
font-family: monospace; |
|
|
} |
|
|
|
|
|
.theme-toggle, .reset-toggle { |
|
|
background: var(--bg-secondary); |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 2px; |
|
|
|
|
|
cursor: pointer; |
|
|
font-family: inherit; |
|
|
font-size: 0.8rem; |
|
|
color: var(--text-secondary); |
|
|
user-select: none; |
|
|
transition: all 0.2s ease; |
|
|
text-transform: lowercase; |
|
|
letter-spacing: 0; |
|
|
} |
|
|
|
|
|
.theme-toggle:hover, .reset-toggle:hover { |
|
|
background: var(--bg-tertiary); |
|
|
border-color: var(--text-secondary); |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.minimap { |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
width: 220px; |
|
|
max-height: 400px; |
|
|
background: var(--bg-secondary); |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 2px; |
|
|
padding: 0.5rem; |
|
|
font-size: 0.7rem; |
|
|
overflow-y: auto; |
|
|
z-index: 100; |
|
|
opacity: 0.9; |
|
|
transition: opacity 0.2s ease; |
|
|
} |
|
|
|
|
|
:root[data-widgets="off"] .controls, |
|
|
:root[data-widgets="off"] .minimap, |
|
|
:root[data-widgets="off"] .file-explorer, |
|
|
:root[data-widgets="off"] .tools-widget, |
|
|
:root[data-widgets="off"] .status-widget { display: none !important; } |
|
|
|
|
|
.file-explorer { |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
left: auto; |
|
|
top: auto; |
|
|
width: 220px; |
|
|
max-height: 400px; |
|
|
background: var(--bg-secondary); |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 2px; |
|
|
padding: 0.5rem; |
|
|
font-size: 0.7rem; |
|
|
overflow-y: auto; |
|
|
z-index: 100; |
|
|
opacity: 0.9; |
|
|
transition: opacity 0.2s ease; |
|
|
} |
|
|
|
|
|
|
|
|
.draw-overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100vw; |
|
|
height: 100vh; |
|
|
z-index: 80; |
|
|
display: block; |
|
|
pointer-events: none; |
|
|
} |
|
|
|
|
|
|
|
|
.tools-widget { |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
left: auto; |
|
|
top: auto; |
|
|
width: 220px; |
|
|
background: var(--bg-secondary); |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 2px; |
|
|
padding: 0.5rem; |
|
|
font-size: 0.7rem; |
|
|
z-index: 100; |
|
|
opacity: 0.95; |
|
|
} |
|
|
.tools-title { |
|
|
font-weight: bold; |
|
|
color: var(--text-secondary); |
|
|
margin-bottom: 0.5rem; |
|
|
padding-bottom: 0.25rem; |
|
|
border-bottom: 1px solid var(--border-primary); |
|
|
cursor: grab; |
|
|
user-select: none; |
|
|
} |
|
|
.tools-row { display: flex; gap: 0.4rem; flex-wrap: wrap; } |
|
|
.tool-button { |
|
|
background: var(--bg-tertiary); |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 2px; |
|
|
padding: 0.25rem 0.4rem; |
|
|
cursor: pointer; |
|
|
color: var(--text-secondary); |
|
|
font-family: inherit; |
|
|
font-size: 0.75rem; |
|
|
user-select: none; |
|
|
} |
|
|
.tool-button:hover { color: var(--text-primary); } |
|
|
.tool-button.active { color: var(--text-primary); border-color: var(--text-secondary); background: var(--bg-secondary); } |
|
|
|
|
|
.minimap:hover, .file-explorer:hover { |
|
|
opacity: 1; |
|
|
} |
|
|
|
|
|
.minimap-title { |
|
|
font-weight: bold; |
|
|
color: var(--text-secondary); |
|
|
margin-bottom: 0.5rem; |
|
|
padding-bottom: 0.25rem; |
|
|
border-bottom: 1px solid var(--border-primary); |
|
|
cursor: grab; |
|
|
user-select: none; |
|
|
} |
|
|
|
|
|
.minimap-item { |
|
|
display: block; |
|
|
color: var(--text-secondary); |
|
|
text-decoration: none; |
|
|
padding: 0.15rem 0; |
|
|
border-left: 2px solid transparent; |
|
|
padding-left: 0.5rem; |
|
|
transition: all 0.2s ease; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.minimap-item:hover { |
|
|
color: var(--text-primary); |
|
|
border-left-color: var(--text-secondary); |
|
|
} |
|
|
|
|
|
.minimap-item.active { |
|
|
color: var(--text-primary); |
|
|
border-left-color: var(--text-link); |
|
|
} |
|
|
|
|
|
.minimap-heading { |
|
|
font-weight: normal; |
|
|
} |
|
|
|
|
|
.minimap-heading.h1 { padding-left: 0.5rem; } |
|
|
.minimap-heading.h2 { padding-left: 1rem; } |
|
|
.minimap-heading.h3 { padding-left: 1.5rem; } |
|
|
.minimap-heading.h4 { padding-left: 2rem; } |
|
|
.minimap-heading.h5 { padding-left: 2.5rem; } |
|
|
.minimap-heading.h6 { padding-left: 3rem; } |
|
|
|
|
|
.minimap-cell { |
|
|
color: var(--text-link); |
|
|
opacity: 0.8; |
|
|
font-style: italic; |
|
|
} |
|
|
|
|
|
.minimap-cell:hover { |
|
|
opacity: 1; |
|
|
} |
|
|
|
|
|
.file-explorer-title { |
|
|
font-weight: bold; |
|
|
color: var(--text-secondary); |
|
|
margin-bottom: 0.5rem; |
|
|
padding-bottom: 0.25rem; |
|
|
border-bottom: 1px solid var(--border-primary); |
|
|
cursor: grab; |
|
|
user-select: none; |
|
|
} |
|
|
|
|
|
.file-explorer-section { |
|
|
margin-bottom: 0.75rem; |
|
|
} |
|
|
|
|
|
.file-explorer-section-title { |
|
|
font-weight: bold; |
|
|
color: var(--text-secondary); |
|
|
font-size: 0.65rem; |
|
|
margin-bottom: 0.25rem; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.5px; |
|
|
} |
|
|
|
|
|
.file-explorer-item { |
|
|
display: block; |
|
|
color: var(--text-secondary); |
|
|
text-decoration: none; |
|
|
padding: 0.1rem 0; |
|
|
margin-left: 0.5rem; |
|
|
transition: color 0.2s ease; |
|
|
cursor: pointer; |
|
|
font-family: monospace; |
|
|
} |
|
|
|
|
|
.file-explorer-item:hover { |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.file-explorer-item.script { |
|
|
color: var(--text-link); |
|
|
} |
|
|
|
|
|
.file-explorer-item.artifact { |
|
|
color: var(--text-secondary); |
|
|
opacity: 0.8; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.minimap, .file-explorer, .tools-widget { |
|
|
display: none; |
|
|
} |
|
|
} |
|
|
|
|
|
.cell { |
|
|
margin: 1rem 0; |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 2px; |
|
|
overflow: hidden; |
|
|
background: var(--bg-secondary); |
|
|
} |
|
|
:root[data-ui="none"] .cell { margin: 1em 0; border: none; background: transparent; } |
|
|
.cell-header { |
|
|
background: var(--bg-secondary); |
|
|
padding: 0.5rem 1rem; |
|
|
border-bottom: 1px solid var(--border-primary); |
|
|
font-family: inherit; |
|
|
font-size: 0.85rem; |
|
|
} |
|
|
:root[data-ui="none"] .cell-header { background: transparent; border: none; padding: 0; font-weight: bold; } |
|
|
:root[data-ui="none"] .cell-content { padding: 0; } |
|
|
:root[data-ui="none"] .copy-button, |
|
|
:root[data-ui="none"] .collapse-indicators, |
|
|
:root[data-ui="none"] .cell-meta, |
|
|
:root[data-ui="none"] .cell-outputs-header { display: none !important; } |
|
|
:root[data-ui="none"] pre, :root[data-ui="none"] code { font-family: Menlo, Monaco, 'Courier New', monospace; } |
|
|
:root[data-ui="none"] .code-content pre { background: #f9f9f9; border: 1px solid #ddd; padding: 8px; } |
|
|
:root[data-ui="none"] .output { background: transparent; border: none; padding: 0.25em 0; } |
|
|
color: var(--text-secondary); |
|
|
cursor: pointer; |
|
|
user-select: none; |
|
|
transition: background-color 0.2s ease; |
|
|
} |
|
|
.cell-header:hover { |
|
|
background: var(--bg-tertiary); |
|
|
} |
|
|
.collapse-indicators { |
|
|
color: var(--text-secondary); |
|
|
font-size: 0.8rem; |
|
|
opacity: 0.7; |
|
|
} |
|
|
.collapse-indicators span:hover { |
|
|
color: var(--text-primary); |
|
|
opacity: 1; |
|
|
} |
|
|
.cell-code { |
|
|
display: block; |
|
|
background: var(--bg-code); |
|
|
} |
|
|
.cell-code.collapsed { |
|
|
display: none; |
|
|
} |
|
|
.cell-code pre { |
|
|
margin: 0; |
|
|
padding: 0.75rem; |
|
|
background: var(--bg-code); |
|
|
overflow-x: auto; |
|
|
color: var(--text-primary); |
|
|
} |
|
|
.cell-output { |
|
|
padding: 0.75rem; |
|
|
|
|
|
background: var(--bg-secondary); |
|
|
} |
|
|
.cell-output.collapsed { |
|
|
display: none; |
|
|
} |
|
|
.cell-stdout { |
|
|
background: var(--bg-tertiary); |
|
|
padding: 0.75rem; |
|
|
border-radius: 1px; |
|
|
|
|
|
font-family: inherit; |
|
|
font-size: 0.9rem; |
|
|
white-space: pre-wrap; |
|
|
color: var(--text-primary); |
|
|
} |
|
|
.cell-stderr { |
|
|
background: var(--bg-error); |
|
|
border-left: 2px solid var(--border-error); |
|
|
padding: 1rem; |
|
|
margin: 0.5rem 0; |
|
|
font-family: inherit; |
|
|
font-size: 0.9rem; |
|
|
color: var(--text-error); |
|
|
white-space: pre-wrap; |
|
|
} |
|
|
.uv-install-logs { |
|
|
margin: 0.5rem 0; |
|
|
} |
|
|
.uv-logs-header { |
|
|
cursor: pointer; |
|
|
padding: 0.75rem; |
|
|
border-left: 3px solid var(--border-color); |
|
|
font-family: inherit; |
|
|
font-size: 0.85rem; |
|
|
color: var(--text-secondary); |
|
|
user-select: none; |
|
|
} |
|
|
.uv-logs-content { |
|
|
background: var(--bg-secondary); |
|
|
padding: 1rem; |
|
|
border-left: 3px solid var(--border-color); |
|
|
white-space: pre-wrap; |
|
|
font-family: monospace; |
|
|
font-size: 0.85rem; |
|
|
color: var(--text-secondary); |
|
|
overflow-x: auto; |
|
|
} |
|
|
.cell-artifacts { |
|
|
margin: 1rem 0; |
|
|
} |
|
|
.cell-artifacts h4 { |
|
|
margin: 0 0 0.5rem 0; |
|
|
color: var(--text-secondary); |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
.artifact { |
|
|
display: inline-block; |
|
|
background: var(--bg-artifact); |
|
|
padding: 0.25rem 0.5rem; |
|
|
border-radius: 1px; |
|
|
margin: 0.25rem 0.5rem 0.25rem 0; |
|
|
font-family: inherit; |
|
|
font-size: 0.8rem; |
|
|
color: var(--text-link); |
|
|
text-decoration: none; |
|
|
transition: background-color 0.2s ease; |
|
|
border: 1px solid var(--border-primary); |
|
|
} |
|
|
.artifact:hover { |
|
|
background: var(--bg-artifact-hover); |
|
|
} |
|
|
.artifact-preview { |
|
|
margin-top: 1rem; |
|
|
} |
|
|
.artifact-preview img { |
|
|
max-width: 100%; |
|
|
height: auto; |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 1px; |
|
|
} |
|
|
.artifact-preview svg { |
|
|
max-width: 100%; |
|
|
height: auto; |
|
|
border: 1px solid var(--border-primary); |
|
|
border-radius: 1px; |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.artifact-preview svg g { |
|
|
fill: var(--text-primary) !important; |
|
|
} |
|
|
|
|
|
.artifact-preview svg { |
|
|
background: transparent; |
|
|
} |
|
|
.cell-failed { |
|
|
border-color: var(--border-cell-failed); |
|
|
} |
|
|
.cell-failed .cell-header { |
|
|
background: var(--bg-error); |
|
|
color: var(--text-error); |
|
|
} |
|
|
.cell-commented { |
|
|
opacity: 0.6; |
|
|
border-style: dashed; |
|
|
} |
|
|
.cell-commented .cell-header { |
|
|
background: var(--bg-secondary); |
|
|
color: var(--text-secondary); |
|
|
font-style: italic; |
|
|
} |
|
|
.run-btn { |
|
|
background: var(--bg-tertiary); |
|
|
border: 1px solid var(--border-primary); |
|
|
padding: 2px 6px; |
|
|
border-radius: 2px; |
|
|
color: var(--text-secondary); |
|
|
cursor: pointer; |
|
|
font-size: 0.75em; |
|
|
font-family: inherit; |
|
|
margin-left: 4px; |
|
|
} |
|
|
.run-btn:hover { |
|
|
color: var(--text-primary); |
|
|
background: var(--bg-primary); |
|
|
} |
|
|
.run-btn:disabled { |
|
|
opacity: 0.6; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
.copy-btn { |
|
|
background: var(--bg-tertiary); |
|
|
border: 1px solid var(--border-primary); |
|
|
padding: 2px 6px; |
|
|
border-radius: 2px; |
|
|
color: var(--text-secondary); |
|
|
cursor: pointer; |
|
|
font-size: 0.75em; |
|
|
font-family: inherit; |
|
|
margin-left: 4px; |
|
|
} |
|
|
.copy-btn:hover { |
|
|
color: var(--text-primary); |
|
|
background: var(--bg-primary); |
|
|
} |
|
|
.copy-btn:disabled { |
|
|
opacity: 0.6; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
.raw-btn { |
|
|
background: var(--bg-tertiary); |
|
|
border: 1px solid var(--border-primary); |
|
|
padding: 2px 6px; |
|
|
border-radius: 2px; |
|
|
color: var(--text-secondary); |
|
|
cursor: pointer; |
|
|
font-size: 0.75em; |
|
|
font-family: inherit; |
|
|
margin-left: 4px; |
|
|
text-decoration: none; |
|
|
display: inline-block; |
|
|
} |
|
|
.raw-btn:hover { |
|
|
color: var(--text-primary); |
|
|
background: var(--bg-primary); |
|
|
text-decoration: none; |
|
|
} |
|
|
.output-stale { |
|
|
opacity: 0.5; |
|
|
position: relative; |
|
|
} |
|
|
.output-stale::after { |
|
|
content: '⏳ updating...'; |
|
|
position: absolute; |
|
|
top: 8px; |
|
|
right: 8px; |
|
|
background: var(--bg-secondary); |
|
|
padding: 4px 8px; |
|
|
border-radius: 2px; |
|
|
font-size: 0.75em; |
|
|
color: var(--text-secondary); |
|
|
border: 1px solid var(--border-primary); |
|
|
} |
|
|
h1, h2, h3, h4, h5, h6 { |
|
|
margin-top: 1.5rem; |
|
|
margin-bottom: 0.75rem; |
|
|
color: var(--text-primary); |
|
|
} |
|
|
h1 { |
|
|
margin-top: 0; |
|
|
margin-bottom: 1rem; |
|
|
} |
|
|
p { |
|
|
margin: 0.75rem 0; |
|
|
color: var(--text-primary); |
|
|
} |
|
|
a { |
|
|
color: var(--text-link); |
|
|
} |
|
|
img { |
|
|
max-width: 100%; |
|
|
height: auto; |
|
|
border-radius: 1px; |
|
|
box-shadow: none; |
|
|
} |
|
|
pre, code { |
|
|
font-family: 'Cascadia Mono', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, 'Consolas', monospace; |
|
|
font-size: var(--code-font-size); |
|
|
} |
|
|
.code-wrap { position: relative; } |
|
|
.code-line-highlight { display: none; position: absolute; left: 0; right: 0; height: 1.5em; background: rgba(255, 235, 170, 0.35); pointer-events: none; border-left: 3px solid #f4c542; } |
|
|
.line-number { cursor: pointer; text-decoration: none; color: var(--text-secondary); padding: 0 0.25rem; } |
|
|
.line-number.selected { background: rgba(255, 235, 170, 0.4); color: var(--text-primary); } |
|
|
|
|
|
|
|
|
.highlight-with-lines { |
|
|
display: flex; |
|
|
} |
|
|
.line-numbers { |
|
|
background: var(--bg-tertiary); |
|
|
padding: var(--code-pad-y) 0.5rem; |
|
|
font-family: 'Cascadia Mono', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, 'Consolas', monospace; |
|
|
font-size: var(--code-font-size); |
|
|
line-height: var(--code-line-height); |
|
|
color: var(--text-secondary); |
|
|
user-select: none; |
|
|
text-align: right; |
|
|
border-right: 1px solid var(--border-primary); |
|
|
} |
|
|
.line-numbers .line-number { |
|
|
display: block; |
|
|
line-height: var(--code-line-height); |
|
|
} |
|
|
.highlight-with-lines .highlight { |
|
|
flex: 1; |
|
|
} |
|
|
.highlight .hll { background-color: transparent; } |
|
|
.highlight pre { |
|
|
white-space: pre; |
|
|
margin: 0; |
|
|
padding: var(--code-pad-y) 0.75rem; |
|
|
line-height: var(--code-line-height); |
|
|
} |
|
|
|
|
|
|
|
|
.cell-code.collapsed { |
|
|
display: none; |
|
|
} |
|
|
.cell-code.expanded { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.cell-code { |
|
|
display: block; |
|
|
border-bottom: 1px solid var(--border-primary); |
|
|
} |
|
|
|
|
|
|
|
|
pre { line-height: 125%; } |
|
|
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } |
|
|
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } |
|
|
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } |
|
|
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } |
|
|
[data-theme="light"] .highlight .hll { background-color: #ffffcc } |
|
|
[data-theme="light"] .highlight { background: #f8f8f8; } |
|
|
[data-theme="light"] .highlight .c { color: #3D7B7B; font-style: italic } |
|
|
[data-theme="light"] .highlight .err { border: 1px solid #F00 } |
|
|
[data-theme="light"] .highlight .k { color: #008000; font-weight: bold } |
|
|
[data-theme="light"] .highlight .o { color: #666 } |
|
|
[data-theme="light"] .highlight .ch { color: #3D7B7B; font-style: italic } |
|
|
[data-theme="light"] .highlight .cm { color: #3D7B7B; font-style: italic } |
|
|
[data-theme="light"] .highlight .cp { color: #9C6500 } |
|
|
[data-theme="light"] .highlight .cpf { color: #3D7B7B; font-style: italic } |
|
|
[data-theme="light"] .highlight .c1 { color: #3D7B7B; font-style: italic } |
|
|
[data-theme="light"] .highlight .cs { color: #3D7B7B; font-style: italic } |
|
|
[data-theme="light"] .highlight .gd { color: #A00000 } |
|
|
[data-theme="light"] .highlight .ge { font-style: italic } |
|
|
[data-theme="light"] .highlight .ges { font-weight: bold; font-style: italic } |
|
|
[data-theme="light"] .highlight .gr { color: #E40000 } |
|
|
[data-theme="light"] .highlight .gh { color: #000080; font-weight: bold } |
|
|
[data-theme="light"] .highlight .gi { color: #008400 } |
|
|
[data-theme="light"] .highlight .go { color: #717171 } |
|
|
[data-theme="light"] .highlight .gp { color: #000080; font-weight: bold } |
|
|
[data-theme="light"] .highlight .gs { font-weight: bold } |
|
|
[data-theme="light"] .highlight .gu { color: #800080; font-weight: bold } |
|
|
[data-theme="light"] .highlight .gt { color: #04D } |
|
|
[data-theme="light"] .highlight .kc { color: #008000; font-weight: bold } |
|
|
[data-theme="light"] .highlight .kd { color: #008000; font-weight: bold } |
|
|
[data-theme="light"] .highlight .kn { color: #008000; font-weight: bold } |
|
|
[data-theme="light"] .highlight .kp { color: #008000 } |
|
|
[data-theme="light"] .highlight .kr { color: #008000; font-weight: bold } |
|
|
[data-theme="light"] .highlight .kt { color: #B00040 } |
|
|
[data-theme="light"] .highlight .m { color: #666 } |
|
|
[data-theme="light"] .highlight .s { color: #BA2121 } |
|
|
[data-theme="light"] .highlight .na { color: #687822 } |
|
|
[data-theme="light"] .highlight .nb { color: #008000 } |
|
|
[data-theme="light"] .highlight .nc { color: #00F; font-weight: bold } |
|
|
[data-theme="light"] .highlight .no { color: #800 } |
|
|
[data-theme="light"] .highlight .nd { color: #A2F } |
|
|
[data-theme="light"] .highlight .ni { color: #717171; font-weight: bold } |
|
|
[data-theme="light"] .highlight .ne { color: #CB3F38; font-weight: bold } |
|
|
[data-theme="light"] .highlight .nf { color: #00F } |
|
|
[data-theme="light"] .highlight .nl { color: #767600 } |
|
|
[data-theme="light"] .highlight .nn { color: #00F; font-weight: bold } |
|
|
[data-theme="light"] .highlight .nt { color: #008000; font-weight: bold } |
|
|
[data-theme="light"] .highlight .nv { color: #19177C } |
|
|
[data-theme="light"] .highlight .ow { color: #A2F; font-weight: bold } |
|
|
[data-theme="light"] .highlight .w { color: #BBB } |
|
|
[data-theme="light"] .highlight .mb { color: #666 } |
|
|
[data-theme="light"] .highlight .mf { color: #666 } |
|
|
[data-theme="light"] .highlight .mh { color: #666 } |
|
|
[data-theme="light"] .highlight .mi { color: #666 } |
|
|
[data-theme="light"] .highlight .mo { color: #666 } |
|
|
[data-theme="light"] .highlight .sa { color: #BA2121 } |
|
|
[data-theme="light"] .highlight .sb { color: #BA2121 } |
|
|
[data-theme="light"] .highlight .sc { color: #BA2121 } |
|
|
[data-theme="light"] .highlight .dl { color: #BA2121 } |
|
|
[data-theme="light"] .highlight .sd { color: #BA2121; font-style: italic } |
|
|
[data-theme="light"] .highlight .s2 { color: #BA2121 } |
|
|
[data-theme="light"] .highlight .se { color: #AA5D1F; font-weight: bold } |
|
|
[data-theme="light"] .highlight .sh { color: #BA2121 } |
|
|
[data-theme="light"] .highlight .si { color: #A45A77; font-weight: bold } |
|
|
[data-theme="light"] .highlight .sx { color: #008000 } |
|
|
[data-theme="light"] .highlight .sr { color: #A45A77 } |
|
|
[data-theme="light"] .highlight .s1 { color: #BA2121 } |
|
|
[data-theme="light"] .highlight .ss { color: #19177C } |
|
|
[data-theme="light"] .highlight .bp { color: #008000 } |
|
|
[data-theme="light"] .highlight .fm { color: #00F } |
|
|
[data-theme="light"] .highlight .vc { color: #19177C } |
|
|
[data-theme="light"] .highlight .vg { color: #19177C } |
|
|
[data-theme="light"] .highlight .vi { color: #19177C } |
|
|
[data-theme="light"] .highlight .vm { color: #19177C } |
|
|
[data-theme="light"] .highlight .il { color: #666 } |
|
|
|
|
|
pre { line-height: 125%; } |
|
|
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } |
|
|
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } |
|
|
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } |
|
|
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } |
|
|
[data-theme="dark"] .highlight .hll { background-color: #49483e } |
|
|
[data-theme="dark"] .highlight { background: #272822; color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .c { color: #959077 } |
|
|
[data-theme="dark"] .highlight .err { color: #ED007E; background-color: #1E0010 } |
|
|
[data-theme="dark"] .highlight .esc { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .g { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .k { color: #66D9EF } |
|
|
[data-theme="dark"] .highlight .l { color: #AE81FF } |
|
|
[data-theme="dark"] .highlight .n { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .o { color: #FF4689 } |
|
|
[data-theme="dark"] .highlight .x { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .p { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .ch { color: #959077 } |
|
|
[data-theme="dark"] .highlight .cm { color: #959077 } |
|
|
[data-theme="dark"] .highlight .cp { color: #959077 } |
|
|
[data-theme="dark"] .highlight .cpf { color: #959077 } |
|
|
[data-theme="dark"] .highlight .c1 { color: #959077 } |
|
|
[data-theme="dark"] .highlight .cs { color: #959077 } |
|
|
[data-theme="dark"] .highlight .gd { color: #FF4689 } |
|
|
[data-theme="dark"] .highlight .ge { color: #F8F8F2; font-style: italic } |
|
|
[data-theme="dark"] .highlight .ges { color: #F8F8F2; font-weight: bold; font-style: italic } |
|
|
[data-theme="dark"] .highlight .gr { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .gh { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .gi { color: #A6E22E } |
|
|
[data-theme="dark"] .highlight .go { color: #66D9EF } |
|
|
[data-theme="dark"] .highlight .gp { color: #FF4689; font-weight: bold } |
|
|
[data-theme="dark"] .highlight .gs { color: #F8F8F2; font-weight: bold } |
|
|
[data-theme="dark"] .highlight .gu { color: #959077 } |
|
|
[data-theme="dark"] .highlight .gt { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .kc { color: #66D9EF } |
|
|
[data-theme="dark"] .highlight .kd { color: #66D9EF } |
|
|
[data-theme="dark"] .highlight .kn { color: #FF4689 } |
|
|
[data-theme="dark"] .highlight .kp { color: #66D9EF } |
|
|
[data-theme="dark"] .highlight .kr { color: #66D9EF } |
|
|
[data-theme="dark"] .highlight .kt { color: #66D9EF } |
|
|
[data-theme="dark"] .highlight .ld { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .m { color: #AE81FF } |
|
|
[data-theme="dark"] .highlight .s { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .na { color: #A6E22E } |
|
|
[data-theme="dark"] .highlight .nb { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .nc { color: #A6E22E } |
|
|
[data-theme="dark"] .highlight .no { color: #66D9EF } |
|
|
[data-theme="dark"] .highlight .nd { color: #A6E22E } |
|
|
[data-theme="dark"] .highlight .ni { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .ne { color: #A6E22E } |
|
|
[data-theme="dark"] .highlight .nf { color: #A6E22E } |
|
|
[data-theme="dark"] .highlight .nl { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .nn { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .nx { color: #A6E22E } |
|
|
[data-theme="dark"] .highlight .py { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .nt { color: #FF4689 } |
|
|
[data-theme="dark"] .highlight .nv { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .ow { color: #FF4689 } |
|
|
[data-theme="dark"] .highlight .pm { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .w { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .mb { color: #AE81FF } |
|
|
[data-theme="dark"] .highlight .mf { color: #AE81FF } |
|
|
[data-theme="dark"] .highlight .mh { color: #AE81FF } |
|
|
[data-theme="dark"] .highlight .mi { color: #AE81FF } |
|
|
[data-theme="dark"] .highlight .mo { color: #AE81FF } |
|
|
[data-theme="dark"] .highlight .sa { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .sb { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .sc { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .dl { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .sd { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .s2 { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .se { color: #AE81FF } |
|
|
[data-theme="dark"] .highlight .sh { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .si { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .sx { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .sr { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .s1 { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .ss { color: #E6DB74 } |
|
|
[data-theme="dark"] .highlight .bp { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .fm { color: #A6E22E } |
|
|
[data-theme="dark"] .highlight .vc { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .vg { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .vi { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .vm { color: #F8F8F2 } |
|
|
[data-theme="dark"] .highlight .il { color: #AE81FF } |
|
|
|
|
|
|
|
|
.highlight pre { |
|
|
white-space: pre; |
|
|
margin: 0; |
|
|
padding: var(--code-pad-y) 0.75rem !important; |
|
|
line-height: var(--code-line-height) !important; |
|
|
font-size: var(--code-font-size) !important; |
|
|
font-family: 'Cascadia Mono', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, 'Consolas', monospace !important; |
|
|
border: none; |
|
|
} |
|
|
.line-numbers { line-height: var(--code-line-height) !important; } |
|
|
.line-numbers .line-number { line-height: var(--code-line-height) !important; } |
|
|
|
|
|
|
|
|
#output-setup { |
|
|
overflow-x: auto; |
|
|
} |
|
|
.cell-stdout { |
|
|
width: 100%; |
|
|
} |
|
|
.cell-stderr { |
|
|
width: max-content; |
|
|
max-height: 300px; |
|
|
overflow: auto; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
body[data-tool="arrow"] .main-content { |
|
|
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23e53935" stroke-width="2"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>') 12 12, crosshair; |
|
|
} |
|
|
body[data-tool="pen"] .main-content { |
|
|
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23e53935" stroke-width="2"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><circle cx="4" cy="20" r="2" fill="%23e53935"/></svg>') 4 20, pointer; |
|
|
} |
|
|
body[data-tool="eraser"] .main-content { |
|
|
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23e53935" stroke-width="2"><path d="M20 20H7l-7-7 7-7h13v14z"/><path d="M13 13l7-7"/><path d="M13 13L9 9"/></svg>') 12 12, auto; |
|
|
} |
|
|
|
|
|
|
|
|
.tools-section-title { |
|
|
font-weight: bold; |
|
|
color: var(--text-secondary); |
|
|
font-size: 0.65rem; |
|
|
margin: 0.75rem 0 0.5rem 0; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.5px; |
|
|
} |
|
|
.color-row { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(6, 1fr); |
|
|
gap: 0.25rem; |
|
|
margin-bottom: 0.5rem; |
|
|
} |
|
|
.color-swatch { |
|
|
width: 18px; |
|
|
height: 18px; |
|
|
border: 2px solid var(--border-primary); |
|
|
border-radius: 3px; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
position: relative; |
|
|
} |
|
|
.color-swatch:hover { |
|
|
transform: scale(1.1); |
|
|
border-color: var(--text-secondary); |
|
|
} |
|
|
.color-swatch.selected { |
|
|
border-color: var(--text-primary); |
|
|
box-shadow: 0 0 0 2px var(--text-link); |
|
|
} |
|
|
.color-swatch.selected::after { |
|
|
content: '✓'; |
|
|
position: absolute; |
|
|
top: 50%; |
|
|
left: 50%; |
|
|
transform: translate(-50%, -50%); |
|
|
color: white; |
|
|
font-size: 10px; |
|
|
font-weight: bold; |
|
|
text-shadow: 1px 1px 1px black; |
|
|
} |
|
|
.color-input { |
|
|
width: 24px; |
|
|
height: 24px; |
|
|
border: 2px solid var(--border-primary); |
|
|
border-radius: 3px; |
|
|
cursor: pointer; |
|
|
background: none; |
|
|
padding: 0; |
|
|
grid-column: span 2; |
|
|
justify-self: center; |
|
|
} |
|
|
.color-input:hover { |
|
|
border-color: var(--text-secondary); |
|
|
} |
|
|
|
|
|
|
|
|
.thickness-row { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
margin-top: 0.75rem; |
|
|
} |
|
|
.thickness-slider { |
|
|
flex: 1; |
|
|
-webkit-appearance: none; |
|
|
appearance: none; |
|
|
height: 4px; |
|
|
background: var(--border-primary); |
|
|
border-radius: 2px; |
|
|
outline: none; |
|
|
opacity: 0.7; |
|
|
transition: opacity 0.2s; |
|
|
} |
|
|
.thickness-slider:hover { |
|
|
opacity: 1; |
|
|
} |
|
|
.thickness-slider::-webkit-slider-thumb { |
|
|
-webkit-appearance: none; |
|
|
appearance: none; |
|
|
width: 12px; |
|
|
height: 12px; |
|
|
background: var(--text-link); |
|
|
border-radius: 50%; |
|
|
cursor: pointer; |
|
|
} |
|
|
.thickness-slider::-moz-range-thumb { |
|
|
width: 12px; |
|
|
height: 12px; |
|
|
background: var(--text-link); |
|
|
border-radius: 50%; |
|
|
cursor: pointer; |
|
|
border: none; |
|
|
} |
|
|
.thickness-value { |
|
|
font-size: 0.7rem; |
|
|
color: var(--text-secondary); |
|
|
min-width: 20px; |
|
|
text-align: right; |
|
|
} |
|
|
|
|
|
.highlight { |
|
|
background: none !important; |
|
|
} |
|
|
|
|
|
|
|
|
.loading-spinner { |
|
|
display: inline-block; |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
border: 2px solid var(--border-primary); |
|
|
border-radius: 50%; |
|
|
border-top-color: var(--text-link); |
|
|
animation: spin 1s linear infinite; |
|
|
margin-right: 8px; |
|
|
vertical-align: middle; |
|
|
} |
|
|
|
|
|
@keyframes spin { |
|
|
to { transform: rotate(360deg); } |
|
|
} |
|
|
|
|
|
.loading-skeleton { |
|
|
display: inline-block; |
|
|
background: var(--bg-tertiary); |
|
|
background: linear-gradient( |
|
|
90deg, |
|
|
var(--bg-tertiary) 25%, |
|
|
var(--bg-secondary) 50%, |
|
|
var(--bg-tertiary) 75% |
|
|
); |
|
|
background-size: 200% 100%; |
|
|
animation: loading-shimmer 2s ease-in-out infinite; |
|
|
border-radius: 2px; |
|
|
height: 1em; |
|
|
width: 80px; |
|
|
vertical-align: middle; |
|
|
} |
|
|
|
|
|
@keyframes loading-shimmer { |
|
|
0% { background-position: -200% 0; } |
|
|
100% { background-position: 200% 0; } |
|
|
} |
|
|
|
|
|
|
|
|
.cell-output:has(.loading-spinner) { |
|
|
opacity: 0.7; |
|
|
background: var(--bg-secondary); |
|
|
|
|
|
} |
|
|
</style> |
|
|
<script> |
|
|
|
|
|
function clamp(val, min, max) { return Math.max(min, Math.min(max, val)); } |
|
|
|
|
|
function restorePosition(el, storageKey) { |
|
|
try { |
|
|
const raw = localStorage.getItem(storageKey); |
|
|
if (!raw) return; |
|
|
const pos = JSON.parse(raw); |
|
|
if (typeof pos.left === 'number' && typeof pos.top === 'number') { |
|
|
el.style.left = pos.left + 'px'; |
|
|
el.style.top = pos.top + 'px'; |
|
|
el.style.right = 'auto'; |
|
|
el.style.bottom = 'auto'; |
|
|
} |
|
|
} catch (_) {} |
|
|
} |
|
|
|
|
|
function savePosition(el, storageKey) { |
|
|
try { |
|
|
const left = parseFloat(el.style.left || 'NaN'); |
|
|
const top = parseFloat(el.style.top || 'NaN'); |
|
|
if (!Number.isNaN(left) && !Number.isNaN(top)) { |
|
|
localStorage.setItem(storageKey, JSON.stringify({ left, top })); |
|
|
} |
|
|
} catch (_) {} |
|
|
} |
|
|
|
|
|
|
|
|
function makeDraggable(el, storageKey, handleEl) { |
|
|
let dragging = false; |
|
|
let startX = 0, startY = 0; |
|
|
let origLeft = 0, origTop = 0; |
|
|
|
|
|
const onMove = (e) => { |
|
|
if (!dragging) return; |
|
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX; |
|
|
const clientY = e.touches ? e.touches[0].clientY : e.clientY; |
|
|
const dx = clientX - startX; |
|
|
const dy = clientY - startY; |
|
|
const w = el.offsetWidth; |
|
|
const h = el.offsetHeight; |
|
|
const maxX = window.innerWidth - w; |
|
|
const maxY = window.innerHeight - h; |
|
|
const newLeft = clamp(origLeft + dx, 0, maxX); |
|
|
const newTop = clamp(origTop + dy, 0, maxY); |
|
|
el.style.left = newLeft + 'px'; |
|
|
el.style.top = newTop + 'px'; |
|
|
el.style.right = 'auto'; |
|
|
el.style.bottom = 'auto'; |
|
|
}; |
|
|
|
|
|
const endDrag = () => { |
|
|
if (!dragging) return; |
|
|
dragging = false; |
|
|
document.removeEventListener('mousemove', onMove); |
|
|
document.removeEventListener('mouseup', endDrag); |
|
|
document.removeEventListener('touchmove', onMove); |
|
|
document.removeEventListener('touchend', endDrag); |
|
|
handleEl && (handleEl.style.cursor = 'grab'); |
|
|
savePosition(el, storageKey); |
|
|
|
|
|
try { layoutWidgetsStackedBottomRight(); } catch (_) {} |
|
|
}; |
|
|
|
|
|
const startDrag = (e) => { |
|
|
|
|
|
const elRect = el.getBoundingClientRect(); |
|
|
el.style.left = elRect.left + 'px'; |
|
|
el.style.top = elRect.top + 'px'; |
|
|
el.style.right = 'auto'; |
|
|
el.style.bottom = 'auto'; |
|
|
|
|
|
dragging = true; |
|
|
startX = e.touches ? e.touches[0].clientX : e.clientX; |
|
|
startY = e.touches ? e.touches[0].clientY : e.clientY; |
|
|
origLeft = elRect.left; |
|
|
origTop = elRect.top; |
|
|
|
|
|
document.addEventListener('mousemove', onMove); |
|
|
document.addEventListener('mouseup', endDrag); |
|
|
document.addEventListener('touchmove', onMove, { passive: false }); |
|
|
document.addEventListener('touchend', endDrag); |
|
|
handleEl && (handleEl.style.cursor = 'grabbing'); |
|
|
e.preventDefault(); |
|
|
}; |
|
|
|
|
|
(handleEl || el).addEventListener('mousedown', startDrag); |
|
|
(handleEl || el).addEventListener('touchstart', startDrag, { passive: false }); |
|
|
|
|
|
|
|
|
restorePosition(el, storageKey); |
|
|
} |
|
|
function toggleCell(cellId) { |
|
|
const codeElement = document.getElementById('code-' + cellId); |
|
|
const outputElement = document.getElementById('output-' + cellId); |
|
|
|
|
|
if (codeElement) { |
|
|
codeElement.classList.toggle('collapsed'); |
|
|
} |
|
|
if (outputElement) { |
|
|
outputElement.classList.toggle('collapsed'); |
|
|
} |
|
|
|
|
|
updateIndicators(cellId); |
|
|
encodeToolStateToUrl(); |
|
|
} |
|
|
|
|
|
function toggleCode(cellId) { |
|
|
const codeElement = document.getElementById('code-' + cellId); |
|
|
if (codeElement) { |
|
|
codeElement.classList.toggle('collapsed'); |
|
|
updateIndicators(cellId); |
|
|
encodeToolStateToUrl(); |
|
|
} |
|
|
} |
|
|
|
|
|
function toggleOutput(cellId) { |
|
|
const outputElement = document.getElementById('output-' + cellId); |
|
|
if (outputElement) { |
|
|
outputElement.classList.toggle('collapsed'); |
|
|
updateIndicators(cellId); |
|
|
encodeToolStateToUrl(); |
|
|
} |
|
|
} |
|
|
|
|
|
function toggleUvLogs(headerElement) { |
|
|
const contentElement = headerElement.nextElementSibling; |
|
|
if (contentElement) { |
|
|
const isCollapsed = contentElement.style.display === 'none'; |
|
|
contentElement.style.display = isCollapsed ? 'block' : 'none'; |
|
|
headerElement.textContent = isCollapsed ? '▼ UV Install Logs' : '▶ UV Install Logs'; |
|
|
|
|
|
|
|
|
const uvLogsDiv = headerElement.parentElement; |
|
|
if (uvLogsDiv && uvLogsDiv.id && uvLogsDiv.id.startsWith('uv-logs-')) { |
|
|
const cellId = uvLogsDiv.id.replace('uv-logs-', ''); |
|
|
const indicatorElement = document.getElementById('uv-indicator-' + cellId); |
|
|
if (indicatorElement) { |
|
|
indicatorElement.textContent = isCollapsed ? '▼ uv-logs' : '▶ uv-logs'; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function toggleUvLogsFromHeader(cellId) { |
|
|
const uvLogsElement = document.getElementById('uv-logs-' + cellId); |
|
|
const indicatorElement = document.getElementById('uv-indicator-' + cellId); |
|
|
if (uvLogsElement) { |
|
|
const headerElement = uvLogsElement.querySelector('.uv-logs-header'); |
|
|
const contentElement = uvLogsElement.querySelector('.uv-logs-content'); |
|
|
if (contentElement && headerElement) { |
|
|
const isCollapsed = contentElement.style.display === 'none'; |
|
|
contentElement.style.display = isCollapsed ? 'block' : 'none'; |
|
|
headerElement.textContent = isCollapsed ? '▼ UV Install Logs' : '▶ UV Install Logs'; |
|
|
if (indicatorElement) { |
|
|
indicatorElement.textContent = isCollapsed ? '▼ uv-logs' : '▶ uv-logs'; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function updateIndicators(cellId) { |
|
|
const codeElement = document.getElementById('code-' + cellId); |
|
|
const outputElement = document.getElementById('output-' + cellId); |
|
|
const indicators = document.querySelector(`[onclick*="${cellId}"]`)?.closest('.cell-header')?.querySelector('.collapse-indicators'); |
|
|
|
|
|
if (indicators) { |
|
|
const codeCollapsed = codeElement && codeElement.classList.contains('collapsed'); |
|
|
const outputCollapsed = outputElement && outputElement.classList.contains('collapsed'); |
|
|
|
|
|
const codeIcon = codeCollapsed ? '▶' : '▼'; |
|
|
const outputIcon = outputCollapsed ? '▶' : '▼'; |
|
|
|
|
|
const codeSpan = indicators.querySelector('[onclick*="toggleCode"]'); |
|
|
const outputSpan = indicators.querySelector('[onclick*="toggleOutput"]'); |
|
|
|
|
|
if (codeSpan) codeSpan.innerHTML = `${codeIcon} code`; |
|
|
if (outputSpan) outputSpan.innerHTML = `${outputIcon} output`; |
|
|
} |
|
|
} |
|
|
|
|
|
function toggleTheme() { |
|
|
const html = document.documentElement; |
|
|
const currentTheme = html.getAttribute('data-theme'); |
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; |
|
|
html.setAttribute('data-theme', newTheme); |
|
|
localStorage.setItem('uvnote-theme', newTheme); |
|
|
updateThemeIcon(); |
|
|
updateUiDebug(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function updateThemeIcon() { |
|
|
const theme = document.documentElement.getAttribute('data-theme'); |
|
|
const toggle = document.querySelector('.theme-toggle'); |
|
|
if (toggle) { |
|
|
toggle.textContent = theme === 'dark' ? 'light' : 'dark'; |
|
|
} |
|
|
} |
|
|
function setUiTheme(newUi) { |
|
|
if (newUi !== 'default' && newUi !== 'none' && newUi !== 'monocolor') return; |
|
|
const html = document.documentElement; |
|
|
html.setAttribute('data-ui', newUi); |
|
|
try { localStorage.setItem('uvnote-ui', newUi); } catch (_) {} |
|
|
updateUiMenu(); |
|
|
updateUiDebug(); |
|
|
} |
|
|
function updateUiMenu() { |
|
|
const ui = document.documentElement.getAttribute('data-ui') || 'default'; |
|
|
const checks = { |
|
|
default: document.getElementById('checkbox-ui-default'), |
|
|
none: document.getElementById('checkbox-ui-none'), |
|
|
monocolor: document.getElementById('checkbox-ui-monocolor') |
|
|
}; |
|
|
if (checks.default) checks.default.textContent = ui === 'default' ? '☑' : '☐'; |
|
|
if (checks.none) checks.none.textContent = ui === 'none' ? '☑' : '☐'; |
|
|
if (checks.monocolor) checks.monocolor.textContent = ui === 'monocolor' ? '☑' : '☐'; |
|
|
} |
|
|
|
|
|
function updateUiDebug() { |
|
|
const ui = document.documentElement.getAttribute('data-ui') || 'default'; |
|
|
const color = document.documentElement.getAttribute('data-theme') || 'light'; |
|
|
const el = document.getElementById('ui-debug'); |
|
|
if (el) { |
|
|
el.textContent = `UI: ${ui} | Color: ${color}`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function clearLineSelections() { |
|
|
try { |
|
|
document.querySelectorAll('.code-line-highlight').forEach(el => { el.style.display = 'none'; }); |
|
|
document.querySelectorAll('.line-number.selected').forEach(el => el.classList.remove('selected')); |
|
|
} catch (_) {} |
|
|
} |
|
|
|
|
|
let _selection = null; |
|
|
|
|
|
function clearSelection(updateUrl) { |
|
|
clearLineSelections(); |
|
|
_selection = null; |
|
|
if (updateUrl) { |
|
|
try { |
|
|
const url = new URL(window.location.href); |
|
|
url.searchParams.delete('cell'); |
|
|
url.searchParams.delete('line'); |
|
|
history.replaceState(null, '', url.toString()); |
|
|
} catch (_) {} |
|
|
} |
|
|
updateStateIndicator(); |
|
|
} |
|
|
|
|
|
function selectCellLine(cellId, line, updateUrl) { |
|
|
try { |
|
|
|
|
|
clearLineSelections(); |
|
|
const codeBox = document.getElementById(`code-${cellId}`); |
|
|
if (!codeBox) return; |
|
|
const pre = codeBox.querySelector('.highlight pre'); |
|
|
const overlay = document.getElementById(`line-highlight-${cellId}`); |
|
|
const numbers = document.getElementById(`lines-${cellId}`); |
|
|
if (!pre || !overlay) return; |
|
|
|
|
|
|
|
|
const preStyle = getComputedStyle(pre); |
|
|
const padTop = parseFloat(preStyle.paddingTop || '0'); |
|
|
const lh = parseFloat(preStyle.lineHeight || '20'); |
|
|
|
|
|
|
|
|
overlay.style.display = 'block'; |
|
|
overlay.style.height = `${lh}px`; |
|
|
overlay.style.top = `${pre.offsetTop + padTop + (line - 1) * lh}px`; |
|
|
|
|
|
|
|
|
if (numbers) { |
|
|
numbers.querySelectorAll('.line-number').forEach(a => a.classList.remove('selected')); |
|
|
const sel = numbers.querySelector(`.line-number[data-line="${line}"]`); |
|
|
if (sel) sel.classList.add('selected'); |
|
|
} |
|
|
|
|
|
if (updateUrl) { |
|
|
const url = new URL(window.location.href); |
|
|
url.searchParams.set('cell', cellId); |
|
|
url.searchParams.set('line', String(line)); |
|
|
history.replaceState(null, '', url.toString()); |
|
|
} |
|
|
_selection = { cellId, a: line, b: line }; |
|
|
updateStateIndicator(); |
|
|
} catch (e) { console.warn('selectCellLine error', e); } |
|
|
} |
|
|
|
|
|
function selectCellLines(cellId, startLine, endLine, updateUrl) { |
|
|
try { |
|
|
|
|
|
const a = Math.min(startLine, endLine); |
|
|
const b = Math.max(startLine, endLine); |
|
|
clearLineSelections(); |
|
|
const codeBox = document.getElementById(`code-${cellId}`); |
|
|
if (!codeBox) return; |
|
|
const pre = codeBox.querySelector('.highlight pre'); |
|
|
const overlay = document.getElementById(`line-highlight-${cellId}`); |
|
|
const numbers = document.getElementById(`lines-${cellId}`); |
|
|
if (!pre || !overlay) return; |
|
|
|
|
|
const preStyle = getComputedStyle(pre); |
|
|
const padTop = parseFloat(preStyle.paddingTop || '0'); |
|
|
const lh = parseFloat(preStyle.lineHeight || '20'); |
|
|
|
|
|
overlay.style.display = 'block'; |
|
|
overlay.style.top = `${pre.offsetTop + padTop + (a - 1) * lh}px`; |
|
|
overlay.style.height = `${(b - a + 1) * lh}px`; |
|
|
|
|
|
if (numbers) { |
|
|
numbers.querySelectorAll('.line-number').forEach(a => a.classList.remove('selected')); |
|
|
for (let i = a; i <= b; i++) { |
|
|
const el = numbers.querySelector(`.line-number[data-line="${i}"]`); |
|
|
if (el) el.classList.add('selected'); |
|
|
} |
|
|
} |
|
|
|
|
|
if (updateUrl) { |
|
|
const url = new URL(window.location.href); |
|
|
url.searchParams.set('cell', cellId); |
|
|
if (a === b) url.searchParams.set('line', String(a)); |
|
|
else url.searchParams.set('line', `${a}-${b}`); |
|
|
history.replaceState(null, '', url.toString()); |
|
|
} |
|
|
_selection = { cellId, a, b }; |
|
|
updateStateIndicator(); |
|
|
} catch (e) { console.warn('selectCellLines error', e); } |
|
|
} |
|
|
|
|
|
|
|
|
let _lineDrag = { active: false, cellId: null, start: 0 }; |
|
|
function onLineNumberMouseDown(e) { |
|
|
const a = e.target.closest('.line-number'); |
|
|
if (!a) return; |
|
|
e.preventDefault(); |
|
|
const cellId = a.dataset.cell; |
|
|
const line = parseInt(a.dataset.line || '1', 10) || 1; |
|
|
|
|
|
const numbers = document.getElementById(`lines-${cellId}`); |
|
|
if (numbers) { |
|
|
const selected = Array.from(numbers.querySelectorAll('.line-number.selected')).map(n => parseInt(n.dataset.line||'0',10)).filter(Boolean); |
|
|
if (selected.length === 1 && selected[0] === line) { |
|
|
clearSelection(true); |
|
|
return; |
|
|
} |
|
|
} |
|
|
_lineDrag.active = true; |
|
|
_lineDrag.cellId = cellId; |
|
|
_lineDrag.start = line; |
|
|
selectCellLines(_lineDrag.cellId, _lineDrag.start, _lineDrag.start, false); |
|
|
} |
|
|
function onDocMouseMove(e) { |
|
|
if (!_lineDrag.active) return; |
|
|
const el = document.elementFromPoint(e.clientX, e.clientY); |
|
|
if (!el) return; |
|
|
const a = el.closest && el.closest('.line-number'); |
|
|
if (!a) return; |
|
|
if (a.dataset.cell !== _lineDrag.cellId) return; |
|
|
const cur = parseInt(a.dataset.line || '1', 10) || 1; |
|
|
selectCellLines(_lineDrag.cellId, _lineDrag.start, cur, false); |
|
|
} |
|
|
function onDocMouseUp(e) { |
|
|
if (!_lineDrag.active) return; |
|
|
const last = document.querySelector('.line-number.selected:last-of-type'); |
|
|
|
|
|
const numbers = document.getElementById(`lines-${_lineDrag.cellId}`); |
|
|
if (numbers) { |
|
|
const selected = Array.from(numbers.querySelectorAll('.line-number.selected')).map(n => parseInt(n.dataset.line||'0',10)).filter(Boolean); |
|
|
if (selected.length) { |
|
|
const a = Math.min(...selected); const b = Math.max(...selected); |
|
|
selectCellLines(_lineDrag.cellId, a, b, true); |
|
|
} |
|
|
} |
|
|
_lineDrag.active = false; _lineDrag.cellId = null; _lineDrag.start = 0; |
|
|
} |
|
|
|
|
|
function applyLocationFromUrl() { |
|
|
try { |
|
|
const url = new URL(window.location.href); |
|
|
const cell = url.searchParams.get('cell'); |
|
|
const lineParam = url.searchParams.get('line'); |
|
|
if (cell && lineParam) { |
|
|
if (lineParam.includes('-')) { |
|
|
const [a, b] = lineParam.split('-').map(x => parseInt(x, 10)); |
|
|
if (!Number.isNaN(a) && !Number.isNaN(b)) selectCellLines(cell, a, b, false); |
|
|
} else { |
|
|
const l = parseInt(lineParam, 10); |
|
|
if (!Number.isNaN(l)) selectCellLine(cell, l, false); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
applyToolsFromUrl(url.searchParams); |
|
|
|
|
|
|
|
|
const encodedCellStates = url.searchParams.get('cells'); |
|
|
console.log('Encoded cell states from URL:', encodedCellStates); |
|
|
} catch (_) {} |
|
|
} |
|
|
|
|
|
function applyToolsFromUrl(params) { |
|
|
try { |
|
|
|
|
|
const showTools = params.get('tools'); |
|
|
if (showTools === '1') { |
|
|
|
|
|
_urlLoadedTool = true; |
|
|
|
|
|
|
|
|
const color = params.get('color'); |
|
|
if (color && /^[0-9a-fA-F]{6}$/.test(color)) { |
|
|
setStoredArrowColor('#' + color); |
|
|
} |
|
|
|
|
|
|
|
|
const thickness = params.get('thickness'); |
|
|
if (thickness) { |
|
|
const value = parseInt(thickness, 10); |
|
|
if (value >= 1 && value <= 10) { |
|
|
setStoredLineThickness(value); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const encodedShapes = params.get('shapes'); |
|
|
if (encodedShapes) { |
|
|
const decodedShapes = decodeShapesFromUrl(encodedShapes); |
|
|
if (decodedShapes.length > 0) { |
|
|
_shapes = decodedShapes; |
|
|
saveShapes(); |
|
|
|
|
|
setTimeout(() => { |
|
|
renderOverlay(); |
|
|
}, 300); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
const toolsWidget = document.querySelector('.tools-widget'); |
|
|
const checkbox = document.getElementById('checkbox-tools'); |
|
|
if (toolsWidget && checkbox) { |
|
|
toolsWidget.style.display = 'block'; |
|
|
checkbox.textContent = '☑'; |
|
|
localStorage.setItem('uvnote-widget-tools', 'visible'); |
|
|
} |
|
|
|
|
|
|
|
|
const activeTool = params.get('tool'); |
|
|
if (activeTool && ['arrow', 'pen', 'eraser', 'spotlight'].includes(activeTool)) { |
|
|
const toolBtn = Array.from(document.querySelectorAll('.tool-button')).find(btn => btn.textContent === activeTool); |
|
|
if (toolBtn) { |
|
|
toolBtn.click(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
layoutWidgetsStackedBottomRight(); |
|
|
}, 200); |
|
|
} |
|
|
} catch (_) {} |
|
|
} |
|
|
|
|
|
function captureInitialCellStates() { |
|
|
const cells = document.querySelectorAll('.cell'); |
|
|
cells.forEach(cell => { |
|
|
const cellId = cell.id.replace('cell-', ''); |
|
|
const codeEl = document.getElementById('code-' + cellId); |
|
|
const outputEl = document.getElementById('output-' + cellId); |
|
|
|
|
|
if (codeEl || outputEl) { |
|
|
const state = {}; |
|
|
if (codeEl) { |
|
|
state.c = codeEl.classList.contains('collapsed') ? 0 : 1; |
|
|
} |
|
|
if (outputEl) { |
|
|
state.o = outputEl.classList.contains('collapsed') ? 0 : 1; |
|
|
} |
|
|
_initialCellStates[cellId] = state; |
|
|
} |
|
|
}); |
|
|
console.log('Captured initial cell states:', _initialCellStates); |
|
|
} |
|
|
|
|
|
function encodeCellStatesToUrl() { |
|
|
|
|
|
const cells = document.querySelectorAll('.cell'); |
|
|
const cellStates = {}; |
|
|
|
|
|
console.log('Found cells:', cells.length); |
|
|
|
|
|
cells.forEach(cell => { |
|
|
const cellId = cell.id.replace('cell-', ''); |
|
|
const codeEl = document.getElementById('code-' + cellId); |
|
|
const outputEl = document.getElementById('output-' + cellId); |
|
|
const initialState = _initialCellStates[cellId] || {}; |
|
|
|
|
|
console.log(`Encoding cell ${cellId}:`, { |
|
|
codeEl: !!codeEl, |
|
|
outputEl: !!outputEl, |
|
|
codeCollapsed: codeEl ? codeEl.classList.contains('collapsed') : 'N/A', |
|
|
outputCollapsed: outputEl ? outputEl.classList.contains('collapsed') : 'N/A', |
|
|
initialState: initialState |
|
|
}); |
|
|
|
|
|
if (codeEl || outputEl) { |
|
|
const state = {}; |
|
|
let hasChanges = false; |
|
|
|
|
|
if (codeEl) { |
|
|
const currentCodeState = codeEl.classList.contains('collapsed') ? 0 : 1; |
|
|
const initialCodeState = initialState.c; |
|
|
|
|
|
if (initialCodeState !== undefined && currentCodeState !== initialCodeState) { |
|
|
state.c = currentCodeState; |
|
|
hasChanges = true; |
|
|
} |
|
|
} |
|
|
|
|
|
if (outputEl) { |
|
|
const currentOutputState = outputEl.classList.contains('collapsed') ? 0 : 1; |
|
|
const initialOutputState = initialState.o; |
|
|
|
|
|
if (initialOutputState !== undefined && currentOutputState !== initialOutputState) { |
|
|
state.o = currentOutputState; |
|
|
hasChanges = true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (hasChanges) { |
|
|
cellStates[cellId] = state; |
|
|
console.log(`Added cell ${cellId}:`, state); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
console.log('Final cell states to encode:', cellStates); |
|
|
|
|
|
|
|
|
if (Object.keys(cellStates).length === 0) return ''; |
|
|
|
|
|
|
|
|
const encoded = btoa(JSON.stringify(cellStates)); |
|
|
console.log('Encoded cell states:', encoded); |
|
|
return encoded; |
|
|
} |
|
|
|
|
|
function decodeCellStatesFromUrl(encodedStates) { |
|
|
if (!encodedStates) return {}; |
|
|
|
|
|
try { |
|
|
return JSON.parse(atob(encodedStates)); |
|
|
} catch (e) { |
|
|
console.error('Failed to decode cell states:', e); |
|
|
return {}; |
|
|
} |
|
|
} |
|
|
|
|
|
function applyCellStatesFromUrl(cellStates) { |
|
|
console.log('Applying cell states from URL:', cellStates); |
|
|
Object.entries(cellStates).forEach(([cellId, state]) => { |
|
|
const codeEl = document.getElementById('code-' + cellId); |
|
|
const outputEl = document.getElementById('output-' + cellId); |
|
|
|
|
|
console.log(`Cell ${cellId}:`, { |
|
|
codeEl: !!codeEl, |
|
|
outputEl: !!outputEl, |
|
|
state: state |
|
|
}); |
|
|
|
|
|
if (codeEl && state.c !== undefined) { |
|
|
if (state.c === 0) { |
|
|
codeEl.classList.add('collapsed'); |
|
|
console.log(`Collapsed code for cell ${cellId}`, { |
|
|
hasCollapsedClass: codeEl.classList.contains('collapsed'), |
|
|
computedDisplay: getComputedStyle(codeEl).display, |
|
|
classList: Array.from(codeEl.classList), |
|
|
elementId: codeEl.id |
|
|
}); |
|
|
} else { |
|
|
codeEl.classList.remove('collapsed'); |
|
|
codeEl.classList.add('expanded'); |
|
|
console.log(`Expanded code for cell ${cellId}`, { |
|
|
hasCollapsedClass: codeEl.classList.contains('collapsed'), |
|
|
hasExpandedClass: codeEl.classList.contains('expanded'), |
|
|
computedDisplay: getComputedStyle(codeEl).display, |
|
|
classList: Array.from(codeEl.classList), |
|
|
elementId: codeEl.id |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
if (outputEl && state.o !== undefined) { |
|
|
if (state.o === 0) { |
|
|
outputEl.classList.add('collapsed'); |
|
|
console.log(`Collapsed output for cell ${cellId}`); |
|
|
} else { |
|
|
outputEl.classList.remove('collapsed'); |
|
|
console.log(`Expanded output for cell ${cellId}`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
updateIndicators(cellId); |
|
|
|
|
|
if (codeEl) { |
|
|
codeEl.offsetHeight; |
|
|
console.log(`After indicators update - code visible: ${getComputedStyle(codeEl).display !== 'none'}`); |
|
|
} |
|
|
if (outputEl) { |
|
|
outputEl.offsetHeight; |
|
|
console.log(`After indicators update - output visible: ${getComputedStyle(outputEl).display !== 'none'}`); |
|
|
} |
|
|
} catch (e) { |
|
|
console.error(`Error updating indicators for cell ${cellId}:`, e); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
function encodeShapesToUrl() { |
|
|
|
|
|
if (_shapes.length === 0) return ''; |
|
|
|
|
|
const shapeData = _shapes.map(shape => { |
|
|
const baseData = { |
|
|
ct: shape.createdAt, |
|
|
fo: shape.fadeoutTime || getFadeoutTime() |
|
|
}; |
|
|
|
|
|
if (shape.type === 'arrow') { |
|
|
return { |
|
|
...baseData, |
|
|
t: 'a', |
|
|
x1: Math.round(shape.x1), |
|
|
y1: Math.round(shape.y1), |
|
|
x2: Math.round(shape.x2), |
|
|
y2: Math.round(shape.y2), |
|
|
c: shape.color.substring(1), |
|
|
w: shape.width |
|
|
}; |
|
|
} else if (shape.type === 'pen') { |
|
|
return { |
|
|
...baseData, |
|
|
t: 'p', |
|
|
pts: shape.points.map(p => [Math.round(p.x), Math.round(p.y)]), |
|
|
c: shape.color.substring(1), |
|
|
w: shape.width |
|
|
}; |
|
|
} else if (shape.type === 'spotlight') { |
|
|
return { |
|
|
...baseData, |
|
|
t: 's', |
|
|
x: Math.round(shape.x), |
|
|
y: Math.round(shape.y), |
|
|
r: Math.round(shape.radius) |
|
|
}; |
|
|
} |
|
|
}).filter(Boolean); |
|
|
|
|
|
return btoa(JSON.stringify(shapeData)); |
|
|
} |
|
|
|
|
|
function decodeShapesFromUrl(encodedShapes) { |
|
|
if (!encodedShapes) return []; |
|
|
|
|
|
try { |
|
|
const shapeData = JSON.parse(atob(encodedShapes)); |
|
|
return shapeData.map(data => { |
|
|
const base = { |
|
|
createdAt: data.ct || Date.now(), |
|
|
fadeoutTime: data.fo || 0, |
|
|
opacity: 1.0 |
|
|
}; |
|
|
|
|
|
if (data.t === 'a') { |
|
|
return { |
|
|
...base, |
|
|
type: 'arrow', |
|
|
x1: data.x1, |
|
|
y1: data.y1, |
|
|
x2: data.x2, |
|
|
y2: data.y2, |
|
|
color: '#' + data.c, |
|
|
width: data.w |
|
|
}; |
|
|
} else if (data.t === 'p') { |
|
|
return { |
|
|
...base, |
|
|
type: 'pen', |
|
|
points: data.pts.map(([x, y]) => ({ x, y })), |
|
|
color: '#' + data.c, |
|
|
width: data.w |
|
|
}; |
|
|
} else if (data.t === 's') { |
|
|
return { |
|
|
...base, |
|
|
type: 'spotlight', |
|
|
x: data.x, |
|
|
y: data.y, |
|
|
radius: data.r, |
|
|
color: '#000000' |
|
|
}; |
|
|
} |
|
|
}).filter(Boolean); |
|
|
} catch (e) { |
|
|
console.error('Failed to decode shapes:', e); |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
function encodeToolStateToUrl() { |
|
|
|
|
|
if (_isInitializing) { |
|
|
return window.location.href; |
|
|
} |
|
|
|
|
|
const params = new URLSearchParams(window.location.search); |
|
|
|
|
|
|
|
|
const toolsWidget = document.querySelector('.tools-widget'); |
|
|
const activeTool = document.body.dataset.tool; |
|
|
const hasActiveTool = activeTool && activeTool !== 'none'; |
|
|
const toolsWidgetVisible = toolsWidget && getComputedStyle(toolsWidget).display !== 'none'; |
|
|
|
|
|
|
|
|
const encodedShapes = encodeShapesToUrl(); |
|
|
if (encodedShapes) { |
|
|
params.set('shapes', encodedShapes); |
|
|
} else { |
|
|
params.delete('shapes'); |
|
|
} |
|
|
|
|
|
|
|
|
const existingCellStates = params.get('cells'); |
|
|
if (existingCellStates) { |
|
|
|
|
|
params.set('cells', existingCellStates); |
|
|
} else { |
|
|
|
|
|
const encodedCellStates = encodeCellStatesToUrl(); |
|
|
if (encodedCellStates) { |
|
|
params.set('cells', encodedCellStates); |
|
|
} |
|
|
} |
|
|
|
|
|
if (toolsWidgetVisible && hasActiveTool) { |
|
|
|
|
|
params.set('tools', '1'); |
|
|
params.set('tool', activeTool); |
|
|
|
|
|
|
|
|
const color = getArrowColor(); |
|
|
if (color && color.startsWith('#')) { |
|
|
params.set('color', color.substring(1)); |
|
|
} |
|
|
|
|
|
|
|
|
const thickness = getLineThickness(); |
|
|
params.set('thickness', thickness.toString()); |
|
|
} else { |
|
|
|
|
|
params.delete('tools'); |
|
|
params.delete('tool'); |
|
|
params.delete('color'); |
|
|
params.delete('thickness'); |
|
|
params.delete('fadeout'); |
|
|
} |
|
|
|
|
|
|
|
|
const newUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : '') + window.location.hash; |
|
|
window.history.replaceState(null, '', newUrl); |
|
|
|
|
|
return window.location.href; |
|
|
} |
|
|
|
|
|
function resetLayout() { |
|
|
try { |
|
|
|
|
|
const allKeys = Object.keys(localStorage); |
|
|
const uvnoteKeys = allKeys.filter(key => key.startsWith('uvnote-')); |
|
|
uvnoteKeys.forEach(k => localStorage.removeItem(k)); |
|
|
} catch (_) {} |
|
|
|
|
|
|
|
|
try { clearSelection(true); } catch(_) {} |
|
|
|
|
|
try { window.setActiveTool('none'); } catch(_) {} |
|
|
|
|
|
try { _shapes = []; saveShapes(); } catch(_) {} |
|
|
|
|
|
try { _urlLoadedTool = false; } catch(_) {} |
|
|
|
|
|
try { |
|
|
const cells = document.querySelectorAll('.cell'); |
|
|
cells.forEach(cell => { |
|
|
const cellId = cell.id.replace('cell-', ''); |
|
|
const codeEl = document.getElementById('code-' + cellId); |
|
|
const outputEl = document.getElementById('output-' + cellId); |
|
|
if (codeEl) codeEl.classList.remove('collapsed'); |
|
|
if (outputEl) outputEl.classList.remove('collapsed'); |
|
|
updateIndicators(cellId); |
|
|
}); |
|
|
} catch(_) {} |
|
|
|
|
|
|
|
|
try { |
|
|
const cleanUrl = window.location.pathname + window.location.hash; |
|
|
window.location.href = cleanUrl; |
|
|
} catch (_) { |
|
|
|
|
|
location.reload(); |
|
|
} |
|
|
} |
|
|
|
|
|
function toggleMenu() { |
|
|
const menuButton = document.querySelector('.menu-button'); |
|
|
if (menuButton) { |
|
|
menuButton.classList.toggle('active'); |
|
|
} |
|
|
} |
|
|
|
|
|
function toggleWidget(widgetName) { |
|
|
let widget; |
|
|
let checkbox; |
|
|
|
|
|
|
|
|
const menuButton = document.querySelector('.menu-button'); |
|
|
if (menuButton) { |
|
|
menuButton.classList.remove('active'); |
|
|
} |
|
|
|
|
|
switch(widgetName) { |
|
|
case 'tools': |
|
|
widget = document.querySelector('.tools-widget'); |
|
|
checkbox = document.getElementById('checkbox-tools'); |
|
|
break; |
|
|
case 'file-explorer': |
|
|
widget = document.querySelector('.file-explorer'); |
|
|
checkbox = document.getElementById('checkbox-file-explorer'); |
|
|
break; |
|
|
case 'minimap': |
|
|
widget = document.querySelector('.minimap'); |
|
|
checkbox = document.getElementById('checkbox-minimap'); |
|
|
break; |
|
|
case 'status': |
|
|
widget = document.querySelector('.status-widget'); |
|
|
checkbox = document.getElementById('checkbox-status'); |
|
|
break; |
|
|
default: |
|
|
return; |
|
|
} |
|
|
|
|
|
if (widget && checkbox) { |
|
|
const isVisible = getComputedStyle(widget).display !== 'none'; |
|
|
widget.style.display = isVisible ? 'none' : 'block'; |
|
|
checkbox.textContent = isVisible ? '☐' : '☑'; |
|
|
|
|
|
|
|
|
try { |
|
|
localStorage.setItem(`uvnote-widget-${widgetName}`, isVisible ? 'hidden' : 'visible'); |
|
|
} catch (_) {} |
|
|
|
|
|
|
|
|
try { |
|
|
layoutWidgetsStackedBottomRight(); |
|
|
} catch (_) {} |
|
|
|
|
|
|
|
|
if (widgetName === 'tools') { |
|
|
encodeToolStateToUrl(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function initializeWidgetVisibility() { |
|
|
const widgets = [ |
|
|
{ name: 'tools', selector: '.tools-widget' }, |
|
|
{ name: 'file-explorer', selector: '.file-explorer' }, |
|
|
{ name: 'minimap', selector: '.minimap' }, |
|
|
{ name: 'status', selector: '.status-widget' } |
|
|
]; |
|
|
|
|
|
widgets.forEach(({ name, selector }) => { |
|
|
const defaultState = name === 'status' ? 'visible' : 'hidden'; |
|
|
const savedState = localStorage.getItem(`uvnote-widget-${name}`) || defaultState; |
|
|
const widget = document.querySelector(selector); |
|
|
const checkbox = document.getElementById(`checkbox-${name}`); |
|
|
|
|
|
if (widget && checkbox) { |
|
|
const isVisible = savedState === 'visible'; |
|
|
widget.style.display = isVisible ? 'block' : 'none'; |
|
|
checkbox.textContent = isVisible ? '☑' : '☐'; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('click', function(event) { |
|
|
const menuButton = document.querySelector('.menu-button'); |
|
|
|
|
|
if (menuButton && !menuButton.contains(event.target)) { |
|
|
menuButton.classList.remove('active'); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
function hasCustomWidgetPositions() { |
|
|
try { |
|
|
return ( |
|
|
localStorage.getItem('uvnote-minimap-pos') || |
|
|
localStorage.getItem('uvnote-file-explorer-pos') || |
|
|
localStorage.getItem('uvnote-tools-pos') |
|
|
); |
|
|
} catch (_) { return false; } |
|
|
} |
|
|
|
|
|
function rectsOverlap(r1, r2) { |
|
|
return !(r1.right <= r2.left || r2.right <= r1.left || r1.bottom <= r2.top || r2.bottom <= r1.top); |
|
|
} |
|
|
|
|
|
function widgetsOverlap(widgets) { |
|
|
for (let i = 0; i < widgets.length; i++) { |
|
|
const a = widgets[i]; |
|
|
const ra = a.getBoundingClientRect(); |
|
|
for (let j = i + 1; j < widgets.length; j++) { |
|
|
const b = widgets[j]; |
|
|
const rb = b.getBoundingClientRect(); |
|
|
if (rectsOverlap(ra, rb)) return true; |
|
|
} |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
function applyStackLayout(widgets, order) { |
|
|
if (!widgets.length) return; |
|
|
|
|
|
const fixedWidth = 220; |
|
|
widgets.forEach(el => { el.style.width = fixedWidth + 'px'; }); |
|
|
|
|
|
|
|
|
const gap = 12; |
|
|
const available = Math.max(0, window.innerHeight - 40 - gap * (order.length - 1)); |
|
|
const eachMax = Math.floor(available / order.length); |
|
|
order.forEach(el => { |
|
|
el.style.maxHeight = eachMax + 'px'; |
|
|
el.style.overflowY = 'auto'; |
|
|
}); |
|
|
|
|
|
|
|
|
let bottomOffset = 20; |
|
|
order.forEach(el => { |
|
|
el.style.left = 'auto'; |
|
|
el.style.top = 'auto'; |
|
|
el.style.right = '20px'; |
|
|
el.style.bottom = bottomOffset + 'px'; |
|
|
bottomOffset += el.offsetHeight + gap; |
|
|
}); |
|
|
} |
|
|
|
|
|
function layoutWidgetsStackedBottomRight() { |
|
|
const minimap = document.querySelector('.minimap'); |
|
|
const fileExplorer = document.querySelector('.file-explorer'); |
|
|
const tools = document.querySelector('.tools-widget'); |
|
|
const status = document.querySelector('.status-widget'); |
|
|
const widgets = [minimap, fileExplorer, tools, status].filter(el => el && getComputedStyle(el).display !== 'none'); |
|
|
if (!widgets.length) return; |
|
|
|
|
|
const order = [minimap, fileExplorer, tools, status].filter(Boolean).filter(el => getComputedStyle(el).display !== 'none'); |
|
|
|
|
|
|
|
|
if (hasCustomWidgetPositions() && !widgetsOverlap(widgets)) return; |
|
|
|
|
|
applyStackLayout(widgets, order); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let _minimapScrollContainer = null; |
|
|
let _minimapScrollHandler = null; |
|
|
function initMinimap() { |
|
|
|
|
|
const minimap = createMinimap(); |
|
|
document.body.appendChild(minimap); |
|
|
|
|
|
const mTitle = minimap.querySelector('.minimap-title'); |
|
|
makeDraggable(minimap, 'uvnote-minimap-pos', mTitle); |
|
|
|
|
|
|
|
|
_minimapScrollContainer = window; |
|
|
|
|
|
if (_minimapScrollContainer) { |
|
|
_minimapScrollHandler = () => updateMinimapActive(); |
|
|
if (_minimapScrollContainer === window) { |
|
|
window.addEventListener('scroll', _minimapScrollHandler); |
|
|
} else { |
|
|
_minimapScrollContainer.addEventListener('scroll', _minimapScrollHandler); |
|
|
} |
|
|
} |
|
|
updateMinimapActive(); |
|
|
} |
|
|
|
|
|
function teardownMinimap() { |
|
|
const minimap = document.querySelector('.minimap'); |
|
|
if (minimap && minimap.parentNode) minimap.parentNode.removeChild(minimap); |
|
|
if (_minimapScrollContainer && _minimapScrollHandler) { |
|
|
if (_minimapScrollContainer === window) { |
|
|
window.removeEventListener('scroll', _minimapScrollHandler); |
|
|
} else { |
|
|
_minimapScrollContainer.removeEventListener('scroll', _minimapScrollHandler); |
|
|
} |
|
|
} |
|
|
_minimapScrollContainer = null; |
|
|
_minimapScrollHandler = null; |
|
|
} |
|
|
|
|
|
function initFileExplorer() { |
|
|
|
|
|
const fileExplorer = createFileExplorer(); |
|
|
document.body.appendChild(fileExplorer); |
|
|
} |
|
|
|
|
|
function createMinimap() { |
|
|
const minimap = document.createElement('div'); |
|
|
minimap.className = 'minimap'; |
|
|
|
|
|
const title = document.createElement('div'); |
|
|
title.className = 'minimap-title'; |
|
|
title.textContent = 'navigation'; |
|
|
minimap.appendChild(title); |
|
|
|
|
|
|
|
|
const root = document.querySelector('.main-content') || document; |
|
|
const headings = root.querySelectorAll('h1, h2, h3, h4, h5, h6'); |
|
|
const cells = root.querySelectorAll('.cell'); |
|
|
|
|
|
|
|
|
const items = []; |
|
|
|
|
|
headings.forEach(heading => { |
|
|
const id = heading.id || generateId(heading.textContent); |
|
|
if (!heading.id) heading.id = id; |
|
|
|
|
|
items.push({ |
|
|
element: heading, |
|
|
type: 'heading', |
|
|
level: parseInt(heading.tagName.charAt(1)), |
|
|
text: heading.textContent.trim(), |
|
|
id: id, |
|
|
position: heading.getBoundingClientRect().top + window.scrollY |
|
|
}); |
|
|
}); |
|
|
|
|
|
cells.forEach(cell => { |
|
|
const header = cell.querySelector('.cell-header'); |
|
|
if (header) { |
|
|
const id = cell.id || `cell-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; |
|
|
if (!cell.id) cell.id = id; |
|
|
|
|
|
items.push({ |
|
|
element: cell, |
|
|
type: 'cell', |
|
|
text: header.textContent.trim(), |
|
|
id: id, |
|
|
position: cell.getBoundingClientRect().top + window.scrollY |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
items.sort((a, b) => a.position - b.position); |
|
|
|
|
|
|
|
|
items.forEach(item => { |
|
|
const link = document.createElement('a'); |
|
|
link.className = `minimap-item ${item.type === 'heading' ? 'minimap-heading' : 'minimap-cell'}`; |
|
|
if (item.type === 'heading') { |
|
|
link.classList.add(`h${item.level}`); |
|
|
} |
|
|
link.textContent = item.text.length > 25 ? item.text.substring(0, 22) + '...' : item.text; |
|
|
link.href = `#${item.id}`; |
|
|
link.onclick = function(e) { |
|
|
e.preventDefault(); |
|
|
item.element.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
|
|
}; |
|
|
minimap.appendChild(link); |
|
|
}); |
|
|
|
|
|
return minimap; |
|
|
} |
|
|
|
|
|
function generateId(text) { |
|
|
return text.toLowerCase() |
|
|
.replace(/[^a-z0-9]+/g, '-') |
|
|
.replace(/^-+|-+$/g, '') |
|
|
.substring(0, 20); |
|
|
} |
|
|
|
|
|
function updateMinimapActive() { |
|
|
const minimapItems = document.querySelectorAll('.minimap-item'); |
|
|
const container = _minimapScrollContainer || window; |
|
|
const containerRect = container === window ? null : container.getBoundingClientRect(); |
|
|
const scrollPos = (container === window ? window.scrollY : container.scrollTop) + 100; |
|
|
|
|
|
let activeItem = null; |
|
|
minimapItems.forEach(item => { |
|
|
const targetId = item.getAttribute('href').substring(1); |
|
|
const target = document.getElementById(targetId); |
|
|
|
|
|
if (target) { |
|
|
const rectTop = target.getBoundingClientRect().top; |
|
|
const targetPos = (container === window) |
|
|
? rectTop + window.scrollY |
|
|
: rectTop - containerRect.top + container.scrollTop; |
|
|
if (targetPos <= scrollPos) { |
|
|
activeItem = item; |
|
|
} |
|
|
} |
|
|
|
|
|
item.classList.remove('active'); |
|
|
}); |
|
|
|
|
|
if (activeItem) { |
|
|
activeItem.classList.add('active'); |
|
|
} |
|
|
} |
|
|
|
|
|
function createFileExplorer() { |
|
|
const fileExplorer = document.createElement('div'); |
|
|
fileExplorer.className = 'file-explorer'; |
|
|
|
|
|
const title = document.createElement('div'); |
|
|
title.className = 'file-explorer-title'; |
|
|
title.textContent = 'files'; |
|
|
fileExplorer.appendChild(title); |
|
|
|
|
|
makeDraggable(fileExplorer, 'uvnote-file-explorer-pos', title); |
|
|
|
|
|
|
|
|
const scriptsSection = document.createElement('div'); |
|
|
scriptsSection.className = 'file-explorer-section'; |
|
|
|
|
|
const scriptsTitle = document.createElement('div'); |
|
|
scriptsTitle.className = 'file-explorer-section-title'; |
|
|
scriptsTitle.textContent = 'scripts'; |
|
|
scriptsSection.appendChild(scriptsTitle); |
|
|
|
|
|
|
|
|
const root = document.querySelector('.main-content') || document; |
|
|
const cells = root.querySelectorAll('.cell'); |
|
|
cells.forEach(cell => { |
|
|
const header = cell.querySelector('.cell-header'); |
|
|
if (header) { |
|
|
const cellText = header.textContent.trim(); |
|
|
const cellMatch = cellText.match(/Cell: ([a-zA-Z_][a-zA-Z0-9_]*)/); |
|
|
if (cellMatch) { |
|
|
const cellId = cellMatch[1]; |
|
|
const scriptItem = document.createElement('div'); |
|
|
scriptItem.className = 'file-explorer-item script'; |
|
|
scriptItem.textContent = `${cellId}.py`; |
|
|
scriptItem.onclick = function() { |
|
|
cell.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
|
|
}; |
|
|
scriptsSection.appendChild(scriptItem); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
fileExplorer.appendChild(scriptsSection); |
|
|
|
|
|
|
|
|
const artifactsSection = document.createElement('div'); |
|
|
artifactsSection.className = 'file-explorer-section'; |
|
|
|
|
|
const artifactsTitle = document.createElement('div'); |
|
|
artifactsTitle.className = 'file-explorer-section-title'; |
|
|
artifactsTitle.textContent = 'artifacts'; |
|
|
artifactsSection.appendChild(artifactsTitle); |
|
|
|
|
|
|
|
|
const artifactsRoot = document.querySelector('.main-content') || document; |
|
|
const artifacts = artifactsRoot.querySelectorAll('.artifact'); |
|
|
if (artifacts.length === 0) { |
|
|
const noArtifacts = document.createElement('div'); |
|
|
noArtifacts.className = 'file-explorer-item artifact'; |
|
|
noArtifacts.textContent = '(none)'; |
|
|
noArtifacts.style.opacity = '0.5'; |
|
|
artifactsSection.appendChild(noArtifacts); |
|
|
} else { |
|
|
artifacts.forEach(artifact => { |
|
|
const artifactItem = document.createElement('div'); |
|
|
artifactItem.className = 'file-explorer-item artifact'; |
|
|
artifactItem.textContent = artifact.textContent; |
|
|
artifactItem.onclick = function() { |
|
|
artifact.click(); |
|
|
}; |
|
|
artifactsSection.appendChild(artifactItem); |
|
|
}); |
|
|
} |
|
|
|
|
|
fileExplorer.appendChild(artifactsSection); |
|
|
|
|
|
return fileExplorer; |
|
|
} |
|
|
|
|
|
function initStatusWidget() { |
|
|
let el = document.querySelector('.status-widget'); |
|
|
if (!el) { |
|
|
el = document.createElement('div'); |
|
|
el.className = 'status-widget'; |
|
|
el.id = 'status-widget'; |
|
|
el.textContent = 'ready — Esc'; |
|
|
document.body.appendChild(el); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let _cursorX = 0; |
|
|
let _cursorY = 0; |
|
|
let _cursorVisible = false; |
|
|
|
|
|
function setActiveTool(tool) { |
|
|
if (!tool || tool === 'none') { |
|
|
document.body.dataset.tool = 'none'; |
|
|
localStorage.setItem('uvnote-active-tool', 'none'); |
|
|
setOverlayActive(false); |
|
|
_cursorVisible = false; |
|
|
|
|
|
const toolButtons = document.querySelectorAll('.tools-widget .tool-button'); |
|
|
toolButtons.forEach(btn => btn.classList.remove('active')); |
|
|
updateStateIndicator(); |
|
|
encodeToolStateToUrl(); |
|
|
return; |
|
|
} |
|
|
document.body.dataset.tool = tool; |
|
|
localStorage.setItem('uvnote-active-tool', tool); |
|
|
setOverlayActive(true); |
|
|
_cursorVisible = true; |
|
|
updateStateIndicator(); |
|
|
encodeToolStateToUrl(); |
|
|
} |
|
|
|
|
|
|
|
|
window.setActiveTool = setActiveTool; |
|
|
|
|
|
|
|
|
function getArrowColor() { |
|
|
const saved = localStorage.getItem('uvnote-arrow-color'); |
|
|
if (saved) return saved; |
|
|
return '#e53935'; |
|
|
} |
|
|
|
|
|
function setStoredArrowColor(color) { |
|
|
try { localStorage.setItem('uvnote-arrow-color', color); } catch (_) {} |
|
|
} |
|
|
|
|
|
function getLineThickness() { |
|
|
const saved = localStorage.getItem('uvnote-line-thickness'); |
|
|
if (saved) return parseInt(saved, 10); |
|
|
return 6; |
|
|
} |
|
|
|
|
|
function setStoredLineThickness(thickness) { |
|
|
try { localStorage.setItem('uvnote-line-thickness', thickness); } catch (_) {} |
|
|
} |
|
|
|
|
|
function getFadeoutTime() { |
|
|
const saved = localStorage.getItem('uvnote-fadeout-time'); |
|
|
if (saved) return parseInt(saved, 10); |
|
|
return 5; |
|
|
} |
|
|
|
|
|
function setStoredFadeoutTime(seconds) { |
|
|
try { localStorage.setItem('uvnote-fadeout-time', seconds); } catch (_) {} |
|
|
} |
|
|
|
|
|
function createToolsWidget() { |
|
|
const tools = document.createElement('div'); |
|
|
tools.className = 'tools-widget'; |
|
|
|
|
|
const title = document.createElement('div'); |
|
|
title.className = 'tools-title'; |
|
|
title.textContent = 'tools'; |
|
|
tools.appendChild(title); |
|
|
|
|
|
const row = document.createElement('div'); |
|
|
row.className = 'tools-row'; |
|
|
tools.appendChild(row); |
|
|
|
|
|
|
|
|
const arrowBtn = document.createElement('div'); |
|
|
arrowBtn.className = 'tool-button'; |
|
|
arrowBtn.textContent = 'arrow'; |
|
|
arrowBtn.onclick = function() { |
|
|
const isActive = arrowBtn.classList.contains('active'); |
|
|
if (isActive) { |
|
|
arrowBtn.classList.remove('active'); |
|
|
setActiveTool('none'); |
|
|
} else { |
|
|
tools.querySelectorAll('.tool-button').forEach(b => b.classList.remove('active')); |
|
|
arrowBtn.classList.add('active'); |
|
|
setActiveTool('arrow'); |
|
|
} |
|
|
}; |
|
|
row.appendChild(arrowBtn); |
|
|
|
|
|
|
|
|
const penBtn = document.createElement('div'); |
|
|
penBtn.className = 'tool-button'; |
|
|
penBtn.textContent = 'pen'; |
|
|
penBtn.onclick = function() { |
|
|
const isActive = penBtn.classList.contains('active'); |
|
|
if (isActive) { |
|
|
penBtn.classList.remove('active'); |
|
|
setActiveTool('none'); |
|
|
} else { |
|
|
tools.querySelectorAll('.tool-button').forEach(b => b.classList.remove('active')); |
|
|
penBtn.classList.add('active'); |
|
|
setActiveTool('pen'); |
|
|
} |
|
|
}; |
|
|
row.appendChild(penBtn); |
|
|
|
|
|
|
|
|
const eraseBtn = document.createElement('div'); |
|
|
eraseBtn.className = 'tool-button'; |
|
|
eraseBtn.textContent = 'eraser'; |
|
|
eraseBtn.onclick = function() { |
|
|
const isActive = eraseBtn.classList.contains('active'); |
|
|
if (isActive) { |
|
|
eraseBtn.classList.remove('active'); |
|
|
setActiveTool('none'); |
|
|
} else { |
|
|
tools.querySelectorAll('.tool-button').forEach(b => b.classList.remove('active')); |
|
|
eraseBtn.classList.add('active'); |
|
|
setActiveTool('eraser'); |
|
|
} |
|
|
}; |
|
|
row.appendChild(eraseBtn); |
|
|
|
|
|
|
|
|
const spotlightBtn = document.createElement('div'); |
|
|
spotlightBtn.className = 'tool-button'; |
|
|
spotlightBtn.textContent = 'spotlight'; |
|
|
spotlightBtn.onclick = function() { |
|
|
const isActive = spotlightBtn.classList.contains('active'); |
|
|
if (isActive) { |
|
|
spotlightBtn.classList.remove('active'); |
|
|
setActiveTool('none'); |
|
|
} else { |
|
|
tools.querySelectorAll('.tool-button').forEach(b => b.classList.remove('active')); |
|
|
spotlightBtn.classList.add('active'); |
|
|
setActiveTool('spotlight'); |
|
|
} |
|
|
}; |
|
|
row.appendChild(spotlightBtn); |
|
|
|
|
|
|
|
|
const clearBtn = document.createElement('div'); |
|
|
clearBtn.className = 'tool-button'; |
|
|
clearBtn.textContent = 'clear'; |
|
|
clearBtn.onclick = function() { |
|
|
_shapes = []; |
|
|
saveShapes(); |
|
|
renderOverlay(); |
|
|
}; |
|
|
row.appendChild(clearBtn); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const saved = localStorage.getItem('uvnote-active-tool') || 'none'; |
|
|
if (saved === 'arrow') { |
|
|
arrowBtn.classList.add('active'); |
|
|
setActiveTool('arrow'); |
|
|
} else if (saved === 'pen') { |
|
|
penBtn.classList.add('active'); |
|
|
setActiveTool('pen'); |
|
|
} else if (saved === 'eraser') { |
|
|
eraseBtn.classList.add('active'); |
|
|
setActiveTool('eraser'); |
|
|
} else if (saved === 'spotlight') { |
|
|
spotlightBtn.classList.add('active'); |
|
|
setActiveTool('spotlight'); |
|
|
} |
|
|
|
|
|
|
|
|
const colorTitle = document.createElement('div'); |
|
|
colorTitle.className = 'tools-section-title'; |
|
|
colorTitle.textContent = 'color'; |
|
|
tools.appendChild(colorTitle); |
|
|
|
|
|
const colorRow = document.createElement('div'); |
|
|
colorRow.className = 'tools-row color-row'; |
|
|
tools.appendChild(colorRow); |
|
|
|
|
|
const swatchColors = [ |
|
|
|
|
|
'#e53935', '#fb8c00', '#fdd835', '#43a047', '#1e88e5', '#8e24aa', |
|
|
|
|
|
'#ff5722', '#795548', '#607d8b', '#9c27b0', |
|
|
|
|
|
'#000000', '#424242', '#9e9e9e', '#ffffff' |
|
|
]; |
|
|
const swatches = []; |
|
|
swatchColors.forEach(c => { |
|
|
const s = document.createElement('div'); |
|
|
s.className = 'color-swatch'; |
|
|
s.style.backgroundColor = c; |
|
|
s.title = c; |
|
|
s.onclick = () => { |
|
|
setStoredArrowColor(c); |
|
|
refreshColorUI(c); |
|
|
if (_cursorVisible) renderOverlay(); |
|
|
encodeToolStateToUrl(); |
|
|
}; |
|
|
colorRow.appendChild(s); |
|
|
swatches.push(s); |
|
|
}); |
|
|
|
|
|
const colorInput = document.createElement('input'); |
|
|
colorInput.type = 'color'; |
|
|
colorInput.className = 'color-input'; |
|
|
colorInput.oninput = () => { |
|
|
setStoredArrowColor(colorInput.value); |
|
|
refreshColorUI(colorInput.value); |
|
|
if (_cursorVisible) renderOverlay(); |
|
|
encodeToolStateToUrl(); |
|
|
}; |
|
|
colorRow.appendChild(colorInput); |
|
|
|
|
|
function refreshColorUI(selected) { |
|
|
const selectedHex = selected.startsWith('#') ? selected.toLowerCase() : rgbToHex(selected); |
|
|
|
|
|
swatches.forEach((s, i) => { |
|
|
const swatchHex = swatchColors[i].toLowerCase(); |
|
|
if (swatchHex === selectedHex) { |
|
|
s.classList.add('selected'); |
|
|
} else { |
|
|
s.classList.remove('selected'); |
|
|
} |
|
|
}); |
|
|
|
|
|
try { |
|
|
colorInput.value = selectedHex; |
|
|
} catch (_) {} |
|
|
} |
|
|
|
|
|
function rgbToHex(rgb) { |
|
|
const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)\)/i); |
|
|
if (!m) return '#000000'; |
|
|
const r = parseInt(m[1]).toString(16).padStart(2, '0'); |
|
|
const g = parseInt(m[2]).toString(16).padStart(2, '0'); |
|
|
const b = parseInt(m[3]).toString(16).padStart(2, '0'); |
|
|
return `#${r}${g}${b}`; |
|
|
} |
|
|
|
|
|
|
|
|
refreshColorUI(getArrowColor()); |
|
|
|
|
|
|
|
|
const thicknessTitle = document.createElement('div'); |
|
|
thicknessTitle.className = 'tools-section-title'; |
|
|
thicknessTitle.textContent = 'thickness'; |
|
|
tools.appendChild(thicknessTitle); |
|
|
|
|
|
const thicknessRow = document.createElement('div'); |
|
|
thicknessRow.className = 'thickness-row'; |
|
|
tools.appendChild(thicknessRow); |
|
|
|
|
|
const thicknessSlider = document.createElement('input'); |
|
|
thicknessSlider.type = 'range'; |
|
|
thicknessSlider.className = 'thickness-slider'; |
|
|
thicknessSlider.min = '1'; |
|
|
thicknessSlider.max = '10'; |
|
|
thicknessSlider.value = getLineThickness(); |
|
|
|
|
|
const thicknessValue = document.createElement('span'); |
|
|
thicknessValue.className = 'thickness-value'; |
|
|
thicknessValue.textContent = thicknessSlider.value + 'px'; |
|
|
|
|
|
thicknessSlider.oninput = function() { |
|
|
const value = parseInt(thicknessSlider.value, 10); |
|
|
setStoredLineThickness(value); |
|
|
thicknessValue.textContent = value + 'px'; |
|
|
if (_cursorVisible) renderOverlay(); |
|
|
encodeToolStateToUrl(); |
|
|
}; |
|
|
|
|
|
thicknessRow.appendChild(thicknessSlider); |
|
|
thicknessRow.appendChild(thicknessValue); |
|
|
|
|
|
|
|
|
const fadeoutTitle = document.createElement('div'); |
|
|
fadeoutTitle.className = 'tools-section-title'; |
|
|
fadeoutTitle.textContent = 'fadeout time'; |
|
|
tools.appendChild(fadeoutTitle); |
|
|
|
|
|
const fadeoutRow = document.createElement('div'); |
|
|
fadeoutRow.className = 'thickness-row'; |
|
|
tools.appendChild(fadeoutRow); |
|
|
|
|
|
const fadeoutSlider = document.createElement('input'); |
|
|
fadeoutSlider.type = 'range'; |
|
|
fadeoutSlider.className = 'thickness-slider'; |
|
|
fadeoutSlider.min = '0'; |
|
|
fadeoutSlider.max = '30'; |
|
|
fadeoutSlider.value = getFadeoutTime(); |
|
|
|
|
|
const fadeoutValue = document.createElement('span'); |
|
|
fadeoutValue.className = 'thickness-value'; |
|
|
fadeoutValue.textContent = fadeoutSlider.value === '0' ? 'never' : fadeoutSlider.value + 's'; |
|
|
|
|
|
fadeoutSlider.oninput = function() { |
|
|
const value = parseInt(fadeoutSlider.value, 10); |
|
|
setStoredFadeoutTime(value); |
|
|
fadeoutValue.textContent = value === 0 ? 'never' : value + 's'; |
|
|
encodeToolStateToUrl(); |
|
|
}; |
|
|
|
|
|
fadeoutRow.appendChild(fadeoutSlider); |
|
|
fadeoutRow.appendChild(fadeoutValue); |
|
|
|
|
|
|
|
|
makeDraggable(tools, 'uvnote-tools-pos', title); |
|
|
|
|
|
return tools; |
|
|
} |
|
|
|
|
|
function initTools() { |
|
|
const widget = createToolsWidget(); |
|
|
document.body.appendChild(widget); |
|
|
} |
|
|
|
|
|
function teardownTools() { |
|
|
const w = document.querySelector('.tools-widget'); |
|
|
if (w && w.parentNode) w.parentNode.removeChild(w); |
|
|
} |
|
|
|
|
|
|
|
|
let _overlay = null; |
|
|
let _overlayCtx = null; |
|
|
let _overlayContainer = null; |
|
|
let _overlayMode = 'single'; |
|
|
let _overlayResizeHandler = null; |
|
|
let _overlayScrollHandler = null; |
|
|
let _drawing = null; |
|
|
let _shapes = []; |
|
|
let _fadeTimer = null; |
|
|
let _urlLoadedTool = false; |
|
|
let _isInitializing = true; |
|
|
let _initialCellStates = {}; |
|
|
|
|
|
function getOverlayStorageKey() { return 'uvnote-shapes'; } |
|
|
|
|
|
function loadShapes() { |
|
|
try { |
|
|
const raw = localStorage.getItem(getOverlayStorageKey()); |
|
|
_shapes = raw ? JSON.parse(raw) : []; |
|
|
} catch (_) { _shapes = []; } |
|
|
} |
|
|
|
|
|
function saveShapes() { |
|
|
try { |
|
|
localStorage.setItem(getOverlayStorageKey(), JSON.stringify(_shapes)); |
|
|
|
|
|
encodeToolStateToUrl(); |
|
|
} catch (_) {} |
|
|
} |
|
|
|
|
|
function updateShapesFade() { |
|
|
const now = Date.now(); |
|
|
let needsUpdate = false; |
|
|
|
|
|
for (let i = _shapes.length - 1; i >= 0; i--) { |
|
|
const shape = _shapes[i]; |
|
|
if (!shape.createdAt) continue; |
|
|
|
|
|
|
|
|
const shapesFadeoutSeconds = shape.fadeoutTime !== undefined ? shape.fadeoutTime : getFadeoutTime(); |
|
|
|
|
|
|
|
|
if (shapesFadeoutSeconds === 0) continue; |
|
|
|
|
|
const fadeStartTime = Math.max(0, (shapesFadeoutSeconds - 2) * 1000); |
|
|
const fadeEndTime = shapesFadeoutSeconds * 1000; |
|
|
const age = now - shape.createdAt; |
|
|
|
|
|
if (age >= fadeEndTime) { |
|
|
|
|
|
_shapes.splice(i, 1); |
|
|
needsUpdate = true; |
|
|
} else if (age >= fadeStartTime) { |
|
|
|
|
|
const fadeProgress = (age - fadeStartTime) / (fadeEndTime - fadeStartTime); |
|
|
const newOpacity = 1 - fadeProgress; |
|
|
if (Math.abs(shape.opacity - newOpacity) > 0.01) { |
|
|
shape.opacity = newOpacity; |
|
|
needsUpdate = true; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (needsUpdate) { |
|
|
saveShapes(); |
|
|
renderOverlay(); |
|
|
|
|
|
encodeToolStateToUrl(); |
|
|
} |
|
|
} |
|
|
|
|
|
function getContentContainer() { return window; } |
|
|
|
|
|
function updateOverlayModeAndContainer() { |
|
|
_overlayContainer = window; |
|
|
_overlayMode = 'single'; |
|
|
} |
|
|
|
|
|
function updateOverlayBounds() { |
|
|
if (!_overlay) return; |
|
|
if (_overlayContainer === window) { |
|
|
_overlay.style.position = 'fixed'; |
|
|
_overlay.style.left = '0px'; |
|
|
_overlay.style.top = '0px'; |
|
|
_overlay.width = window.innerWidth; |
|
|
_overlay.height = window.innerHeight; |
|
|
} else { |
|
|
const rect = _overlayContainer.getBoundingClientRect(); |
|
|
_overlay.style.position = 'fixed'; |
|
|
_overlay.style.left = rect.left + 'px'; |
|
|
_overlay.style.top = rect.top + 'px'; |
|
|
_overlay.width = Math.max(0, Math.floor(rect.width)); |
|
|
_overlay.height = Math.max(0, Math.floor(rect.height)); |
|
|
} |
|
|
renderOverlay(); |
|
|
} |
|
|
|
|
|
function containerScrollLeft() { |
|
|
return (_overlayContainer === window) ? (window.scrollX || 0) : (_overlayContainer.scrollLeft || 0); |
|
|
} |
|
|
function containerScrollTop() { |
|
|
return (_overlayContainer === window) ? (window.scrollY || 0) : (_overlayContainer.scrollTop || 0); |
|
|
} |
|
|
|
|
|
function toCanvasCoords(clientX, clientY) { |
|
|
const rect = _overlay.getBoundingClientRect(); |
|
|
return { x: clientX - rect.left, y: clientY - rect.top }; |
|
|
} |
|
|
|
|
|
function onPointerDown(e) { |
|
|
const tool = document.body.dataset.tool; |
|
|
if (tool === 'arrow') { |
|
|
startDrawArrow(e); |
|
|
} else if (tool === 'pen') { |
|
|
startDrawPen(e); |
|
|
} else if (tool === 'eraser') { |
|
|
eraseAt(e); |
|
|
} else if (tool === 'spotlight') { |
|
|
startDrawSpotlight(e); |
|
|
} |
|
|
} |
|
|
|
|
|
function onPointerMove(e) { |
|
|
|
|
|
const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); |
|
|
_cursorX = pt.x; |
|
|
_cursorY = pt.y; |
|
|
|
|
|
if (!_drawing) { |
|
|
|
|
|
if (_cursorVisible) { |
|
|
renderOverlay(); |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
if (_drawing.type === 'pen') { |
|
|
moveDrawPen(e); |
|
|
} else if (_drawing.type === 'spotlight') { |
|
|
moveDrawSpotlight(e); |
|
|
} else { |
|
|
moveDrawArrow(e); |
|
|
} |
|
|
} |
|
|
|
|
|
function onPointerEnter(e) { |
|
|
_cursorVisible = document.body.dataset.tool !== 'none'; |
|
|
if (_cursorVisible) { |
|
|
renderOverlay(); |
|
|
} |
|
|
} |
|
|
|
|
|
function onPointerLeave(e) { |
|
|
_cursorVisible = false; |
|
|
renderOverlay(); |
|
|
} |
|
|
|
|
|
function onPointerUp(e) { |
|
|
if (!_drawing) return; |
|
|
if (_drawing.type === 'pen') { |
|
|
endDrawPen(); |
|
|
} else if (_drawing.type === 'spotlight') { |
|
|
endDrawSpotlight(); |
|
|
} else { |
|
|
endDrawArrow(); |
|
|
} |
|
|
} |
|
|
|
|
|
function startDrawArrow(e) { |
|
|
if (document.body.dataset.tool !== 'arrow') return; |
|
|
const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); |
|
|
_drawing = { |
|
|
x1: pt.x + containerScrollLeft(), |
|
|
y1: pt.y + containerScrollTop(), |
|
|
x2: pt.x + containerScrollLeft(), |
|
|
y2: pt.y + containerScrollTop(), |
|
|
color: getArrowColor(), |
|
|
width: getLineThickness() |
|
|
}; |
|
|
renderOverlay(); |
|
|
e.preventDefault(); |
|
|
} |
|
|
|
|
|
function moveDrawArrow(e) { |
|
|
if (!_drawing) return; |
|
|
const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); |
|
|
_drawing.x2 = pt.x + containerScrollLeft(); |
|
|
_drawing.y2 = pt.y + containerScrollTop(); |
|
|
renderOverlay(); |
|
|
e.preventDefault(); |
|
|
} |
|
|
|
|
|
function endDrawArrow() { |
|
|
if (!_drawing) return; |
|
|
_shapes.push({ |
|
|
type: 'arrow', |
|
|
..._drawing, |
|
|
createdAt: Date.now(), |
|
|
fadeoutTime: getFadeoutTime(), |
|
|
opacity: 1.0 |
|
|
}); |
|
|
_drawing = null; |
|
|
saveShapes(); |
|
|
renderOverlay(); |
|
|
} |
|
|
|
|
|
function startDrawPen(e) { |
|
|
if (document.body.dataset.tool !== 'pen') return; |
|
|
const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); |
|
|
_drawing = { |
|
|
type: 'pen', |
|
|
points: [{ |
|
|
x: pt.x + containerScrollLeft(), |
|
|
y: pt.y + containerScrollTop() |
|
|
}], |
|
|
color: getArrowColor(), |
|
|
width: getLineThickness() |
|
|
}; |
|
|
renderOverlay(); |
|
|
e.preventDefault(); |
|
|
} |
|
|
|
|
|
function moveDrawPen(e) { |
|
|
if (!_drawing || _drawing.type !== 'pen') return; |
|
|
const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); |
|
|
_drawing.points.push({ |
|
|
x: pt.x + containerScrollLeft(), |
|
|
y: pt.y + containerScrollTop() |
|
|
}); |
|
|
renderOverlay(); |
|
|
e.preventDefault(); |
|
|
} |
|
|
|
|
|
function endDrawPen() { |
|
|
if (!_drawing || _drawing.type !== 'pen') return; |
|
|
if (_drawing.points.length > 1) { |
|
|
_shapes.push({ |
|
|
..._drawing, |
|
|
createdAt: Date.now(), |
|
|
fadeoutTime: getFadeoutTime(), |
|
|
opacity: 1.0 |
|
|
}); |
|
|
} |
|
|
_drawing = null; |
|
|
saveShapes(); |
|
|
renderOverlay(); |
|
|
} |
|
|
|
|
|
function startDrawSpotlight(e) { |
|
|
if (document.body.dataset.tool !== 'spotlight') return; |
|
|
const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); |
|
|
_drawing = { |
|
|
type: 'spotlight', |
|
|
x: pt.x + containerScrollLeft(), |
|
|
y: pt.y + containerScrollTop(), |
|
|
radius: getLineThickness() * 20, |
|
|
color: getArrowColor() |
|
|
}; |
|
|
renderOverlay(); |
|
|
e.preventDefault(); |
|
|
} |
|
|
|
|
|
function moveDrawSpotlight(e) { |
|
|
if (!_drawing || _drawing.type !== 'spotlight') return; |
|
|
const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); |
|
|
const dx = pt.x + containerScrollLeft() - _drawing.x; |
|
|
const dy = pt.y + containerScrollTop() - _drawing.y; |
|
|
_drawing.radius = Math.max(20, Math.sqrt(dx * dx + dy * dy)); |
|
|
renderOverlay(); |
|
|
e.preventDefault(); |
|
|
} |
|
|
|
|
|
function endDrawSpotlight() { |
|
|
if (!_drawing || _drawing.type !== 'spotlight') return; |
|
|
_shapes.push({ |
|
|
..._drawing, |
|
|
createdAt: Date.now(), |
|
|
fadeoutTime: getFadeoutTime(), |
|
|
opacity: 1.0 |
|
|
}); |
|
|
_drawing = null; |
|
|
saveShapes(); |
|
|
renderOverlay(); |
|
|
} |
|
|
|
|
|
function distPointToSegment(px, py, x1, y1, x2, y2) { |
|
|
const dx = x2 - x1, dy = y2 - y1; |
|
|
if (dx === 0 && dy === 0) return Math.hypot(px - x1, py - y1); |
|
|
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / (dx*dx + dy*dy))); |
|
|
const cx = x1 + t * dx, cy = y1 + t * dy; |
|
|
return Math.hypot(px - cx, py - cy); |
|
|
} |
|
|
|
|
|
function eraseAt(e) { |
|
|
const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); |
|
|
const x = pt.x + containerScrollLeft(); |
|
|
const y = pt.y + containerScrollTop(); |
|
|
const threshold = 10; |
|
|
for (let i = _shapes.length - 1; i >= 0; i--) { |
|
|
const s = _shapes[i]; |
|
|
if (s.type === 'arrow') { |
|
|
const d = distPointToSegment(x, y, s.x1, s.y1, s.x2, s.y2); |
|
|
if (d <= threshold) { |
|
|
_shapes.splice(i, 1); |
|
|
saveShapes(); |
|
|
renderOverlay(); |
|
|
break; |
|
|
} |
|
|
} else if (s.type === 'pen' && s.points) { |
|
|
|
|
|
let minDist = Infinity; |
|
|
for (let j = 1; j < s.points.length; j++) { |
|
|
const d = distPointToSegment(x, y, s.points[j-1].x, s.points[j-1].y, s.points[j].x, s.points[j].y); |
|
|
minDist = Math.min(minDist, d); |
|
|
} |
|
|
if (minDist <= threshold) { |
|
|
_shapes.splice(i, 1); |
|
|
saveShapes(); |
|
|
renderOverlay(); |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
e.preventDefault(); |
|
|
} |
|
|
|
|
|
function drawArrow(ctx, x1, y1, x2, y2, color, width, opacity = 1.0) { |
|
|
|
|
|
const oldAlpha = ctx.globalAlpha; |
|
|
ctx.globalAlpha = opacity; |
|
|
|
|
|
ctx.strokeStyle = color; |
|
|
ctx.fillStyle = color; |
|
|
ctx.lineWidth = width; |
|
|
ctx.lineCap = 'round'; |
|
|
ctx.lineJoin = 'round'; |
|
|
|
|
|
|
|
|
const dx = x2 - x1; |
|
|
const dy = y2 - y1; |
|
|
const distance = Math.sqrt(dx * dx + dy * dy); |
|
|
|
|
|
if (distance < 5) { |
|
|
|
|
|
const defaultAngle = Math.PI / 4; |
|
|
const headLength = Math.min(15 + width * 1.5, 25); |
|
|
const headAngle = Math.PI / 6; |
|
|
|
|
|
|
|
|
const hx1 = x1 + headLength * Math.cos(defaultAngle - headAngle); |
|
|
const hy1 = y1 + headLength * Math.sin(defaultAngle - headAngle); |
|
|
const hx2 = x1 + headLength * Math.cos(defaultAngle + headAngle); |
|
|
const hy2 = y1 + headLength * Math.sin(defaultAngle + headAngle); |
|
|
|
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(x1, y1); |
|
|
ctx.lineTo(hx1, hy1); |
|
|
ctx.lineTo(hx2, hy2); |
|
|
ctx.closePath(); |
|
|
ctx.fill(); |
|
|
} else { |
|
|
|
|
|
const angle = Math.atan2(y1 - y2, x1 - x2); |
|
|
const headLength = Math.min(15 + width * 1.5, 25); |
|
|
const headAngle = Math.PI / 6; |
|
|
|
|
|
|
|
|
const lineEndX = x1 - headLength * 0.8 * Math.cos(angle); |
|
|
const lineEndY = y1 - headLength * 0.8 * Math.sin(angle); |
|
|
|
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(x2, y2); |
|
|
ctx.lineTo(lineEndX, lineEndY); |
|
|
ctx.stroke(); |
|
|
|
|
|
|
|
|
const hx1 = x1 - headLength * Math.cos(angle - headAngle); |
|
|
const hy1 = y1 - headLength * Math.sin(angle - headAngle); |
|
|
const hx2 = x1 - headLength * Math.cos(angle + headAngle); |
|
|
const hy2 = y1 - headLength * Math.sin(angle + headAngle); |
|
|
|
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(x1, y1); |
|
|
ctx.lineTo(hx1, hy1); |
|
|
ctx.lineTo(hx2, hy2); |
|
|
ctx.closePath(); |
|
|
ctx.fill(); |
|
|
} |
|
|
|
|
|
|
|
|
ctx.globalAlpha = oldAlpha; |
|
|
} |
|
|
|
|
|
function drawPen(ctx, points, color, width, offX, offY, opacity = 1.0) { |
|
|
if (!points || points.length < 2) return; |
|
|
|
|
|
|
|
|
const oldAlpha = ctx.globalAlpha; |
|
|
ctx.globalAlpha = opacity; |
|
|
|
|
|
ctx.strokeStyle = color; |
|
|
ctx.lineWidth = width; |
|
|
ctx.lineCap = 'round'; |
|
|
ctx.lineJoin = 'round'; |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(points[0].x - offX, points[0].y - offY); |
|
|
for (let i = 1; i < points.length; i++) { |
|
|
ctx.lineTo(points[i].x - offX, points[i].y - offY); |
|
|
} |
|
|
ctx.stroke(); |
|
|
|
|
|
|
|
|
ctx.globalAlpha = oldAlpha; |
|
|
} |
|
|
|
|
|
function drawAllSpotlights(ctx, spotlights, offX, offY) { |
|
|
if (!spotlights || spotlights.length === 0) return; |
|
|
|
|
|
ctx.save(); |
|
|
|
|
|
|
|
|
const maxOpacity = Math.max(...spotlights.map(s => s.opacity || 1.0)); |
|
|
|
|
|
|
|
|
ctx.fillStyle = `rgba(0, 0, 0, ${0.7 * maxOpacity})`; |
|
|
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); |
|
|
|
|
|
|
|
|
ctx.globalCompositeOperation = 'destination-out'; |
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 1)'; |
|
|
for (const spotlight of spotlights) { |
|
|
ctx.beginPath(); |
|
|
ctx.arc(spotlight.x - offX, spotlight.y - offY, spotlight.radius, 0, 2 * Math.PI); |
|
|
ctx.fill(); |
|
|
} |
|
|
|
|
|
ctx.restore(); |
|
|
} |
|
|
|
|
|
function renderOverlay() { |
|
|
if (!_overlay || !_overlayCtx) return; |
|
|
_overlayCtx.clearRect(0, 0, _overlay.width, _overlay.height); |
|
|
const offX = containerScrollLeft(); |
|
|
const offY = containerScrollTop(); |
|
|
|
|
|
for (const s of _shapes) { |
|
|
const opacity = s.opacity !== undefined ? s.opacity : 1.0; |
|
|
if (s.type === 'arrow') { |
|
|
drawArrow(_overlayCtx, s.x1 - offX, s.y1 - offY, s.x2 - offX, s.y2 - offY, s.color || '#f00', s.width || 2, opacity); |
|
|
} else if (s.type === 'pen') { |
|
|
drawPen(_overlayCtx, s.points, s.color || '#f00', s.width || 2, offX, offY, opacity); |
|
|
} |
|
|
} |
|
|
|
|
|
if (_drawing) { |
|
|
if (_drawing.type === 'pen') { |
|
|
drawPen(_overlayCtx, _drawing.points, _drawing.color, _drawing.width, offX, offY); |
|
|
} else if (_drawing.type !== 'spotlight') { |
|
|
drawArrow(_overlayCtx, _drawing.x1 - offX, _drawing.y1 - offY, _drawing.x2 - offX, _drawing.y2 - offY, _drawing.color, _drawing.width); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const spotlights = []; |
|
|
|
|
|
|
|
|
for (const s of _shapes) { |
|
|
if (s.type === 'spotlight') { |
|
|
spotlights.push({ |
|
|
x: s.x, |
|
|
y: s.y, |
|
|
radius: s.radius, |
|
|
opacity: s.opacity !== undefined ? s.opacity : 1.0 |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (_drawing && _drawing.type === 'spotlight') { |
|
|
spotlights.push({ |
|
|
x: _drawing.x, |
|
|
y: _drawing.y, |
|
|
radius: _drawing.radius, |
|
|
opacity: 1.0 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (_cursorVisible && !_drawing) { |
|
|
const tool = document.body.dataset.tool; |
|
|
if (tool === 'spotlight') { |
|
|
const thickness = getLineThickness(); |
|
|
const radius = thickness * 20; |
|
|
const cursorWorldX = _cursorX + containerScrollLeft(); |
|
|
const cursorWorldY = _cursorY + containerScrollTop(); |
|
|
spotlights.push({ |
|
|
x: cursorWorldX, |
|
|
y: cursorWorldY, |
|
|
radius: radius, |
|
|
opacity: 0.8 |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
drawAllSpotlights(_overlayCtx, spotlights, offX, offY); |
|
|
|
|
|
|
|
|
if (_cursorVisible && !_drawing) { |
|
|
const tool = document.body.dataset.tool; |
|
|
const color = getArrowColor(); |
|
|
const thickness = getLineThickness(); |
|
|
|
|
|
if (tool !== 'spotlight') { |
|
|
_overlayCtx.save(); |
|
|
_overlayCtx.fillStyle = color; |
|
|
_overlayCtx.globalAlpha = 0.7; |
|
|
|
|
|
if (tool === 'eraser') { |
|
|
|
|
|
_overlayCtx.strokeStyle = color; |
|
|
_overlayCtx.lineWidth = 2; |
|
|
_overlayCtx.beginPath(); |
|
|
_overlayCtx.arc(_cursorX, _cursorY, 10, 0, 2 * Math.PI); |
|
|
_overlayCtx.stroke(); |
|
|
} else { |
|
|
|
|
|
_overlayCtx.beginPath(); |
|
|
_overlayCtx.arc(_cursorX, _cursorY, thickness / 2, 0, 2 * Math.PI); |
|
|
_overlayCtx.fill(); |
|
|
} |
|
|
|
|
|
_overlayCtx.restore(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function setOverlayActive(active) { |
|
|
if (!_overlay) initOverlay(); |
|
|
_overlay.style.pointerEvents = active ? 'auto' : 'none'; |
|
|
_overlay.style.cursor = active ? 'none' : 'auto'; |
|
|
|
|
|
renderOverlay(); |
|
|
} |
|
|
|
|
|
function initOverlay() { |
|
|
if (_overlay) return; |
|
|
updateOverlayModeAndContainer(); |
|
|
_overlay = document.createElement('canvas'); |
|
|
_overlay.className = 'draw-overlay'; |
|
|
_overlayCtx = _overlay.getContext('2d'); |
|
|
document.body.appendChild(_overlay); |
|
|
updateOverlayBounds(); |
|
|
loadShapes(); |
|
|
renderOverlay(); |
|
|
|
|
|
|
|
|
_overlay.addEventListener('mousedown', onPointerDown); |
|
|
_overlay.addEventListener('mousemove', onPointerMove); |
|
|
_overlay.addEventListener('mouseenter', onPointerEnter); |
|
|
_overlay.addEventListener('mouseleave', onPointerLeave); |
|
|
document.addEventListener('mouseup', onPointerUp); |
|
|
_overlay.addEventListener('touchstart', onPointerDown, { passive: false }); |
|
|
_overlay.addEventListener('touchmove', onPointerMove, { passive: false }); |
|
|
document.addEventListener('touchend', onPointerUp); |
|
|
|
|
|
_overlayResizeHandler = () => updateOverlayBounds(); |
|
|
window.addEventListener('resize', _overlayResizeHandler); |
|
|
|
|
|
_overlayScrollHandler = () => renderOverlay(); |
|
|
window.addEventListener('scroll', _overlayScrollHandler); |
|
|
|
|
|
|
|
|
_fadeTimer = setInterval(updateShapesFade, 100); |
|
|
} |
|
|
|
|
|
function rebindOverlayContainer() { |
|
|
if (!_overlay) return; |
|
|
|
|
|
if (_overlayScrollHandler) { window.removeEventListener('scroll', _overlayScrollHandler); } |
|
|
updateOverlayModeAndContainer(); |
|
|
updateOverlayBounds(); |
|
|
loadShapes(); |
|
|
renderOverlay(); |
|
|
_overlayScrollHandler = () => renderOverlay(); |
|
|
window.addEventListener('scroll', _overlayScrollHandler); |
|
|
} |
|
|
|
|
|
function teardownOverlay() { |
|
|
if (!_overlay) return; |
|
|
_overlay.removeEventListener('mousedown', onPointerDown); |
|
|
_overlay.removeEventListener('mousemove', onPointerMove); |
|
|
_overlay.removeEventListener('mouseenter', onPointerEnter); |
|
|
_overlay.removeEventListener('mouseleave', onPointerLeave); |
|
|
document.removeEventListener('mouseup', onPointerUp); |
|
|
_overlay.removeEventListener('touchstart', onPointerDown); |
|
|
_overlay.removeEventListener('touchmove', onPointerMove); |
|
|
document.removeEventListener('touchend', onPointerUp); |
|
|
if (_overlayResizeHandler) window.removeEventListener('resize', _overlayResizeHandler); |
|
|
if (_overlayScrollHandler) { |
|
|
if (_overlayContainer === window) { |
|
|
window.removeEventListener('scroll', _overlayScrollHandler); |
|
|
} else if (_overlayContainer) { |
|
|
_overlayContainer.removeEventListener('scroll', _overlayScrollHandler); |
|
|
} |
|
|
} |
|
|
if (_fadeTimer) { |
|
|
clearInterval(_fadeTimer); |
|
|
_fadeTimer = null; |
|
|
} |
|
|
if (_overlay.parentNode) _overlay.parentNode.removeChild(_overlay); |
|
|
_overlay = null; _overlayCtx = null; _overlayContainer = null; _overlayResizeHandler = null; _overlayScrollHandler = null; _drawing = null; |
|
|
} |
|
|
|
|
|
function teardownFileExplorer() { |
|
|
const fe = document.querySelector('.file-explorer'); |
|
|
if (fe && fe.parentNode) fe.parentNode.removeChild(fe); |
|
|
} |
|
|
|
|
|
function escapeHtml(text) { |
|
|
const div = document.createElement('div'); |
|
|
div.textContent = text; |
|
|
return div.innerHTML; |
|
|
} |
|
|
|
|
|
function runCell(cellId){ |
|
|
const btn=document.querySelector('.run-btn[onclick*="'+cellId+'"]'); |
|
|
const output=document.getElementById('output-'+cellId); |
|
|
if(btn){btn.textContent='⏳ running...';btn.disabled=true;} |
|
|
if(output){output.classList.add('output-stale');} |
|
|
fetch('/run/'+cellId,{method:'POST'}).then(r=>r.json()).then(data=>{ |
|
|
if(output){ |
|
|
output.classList.remove('output-stale'); |
|
|
let html=''; |
|
|
if(data.stdout) html+='<div class="cell-stdout">'+escapeHtml(data.stdout)+'</div>'; |
|
|
console.log('UV Logs:', data); |
|
|
if(data.stderr) { |
|
|
|
|
|
const lines = data.stderr.split('\\n'); |
|
|
let uvLogs = []; |
|
|
let regularLogs = []; |
|
|
let inUvSection = true; |
|
|
|
|
|
for (const line of lines) { |
|
|
if (inUvSection) { |
|
|
uvLogs.push(line); |
|
|
if (line.startsWith('Installed ')) { |
|
|
inUvSection = false; |
|
|
} |
|
|
} else { |
|
|
regularLogs.push(line); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (inUvSection) { |
|
|
html+='<div class="cell-stderr">'+escapeHtml(data.stderr)+'</div>'; |
|
|
} else { |
|
|
const uvLogsStr = uvLogs.join('\\n'); |
|
|
const regularLogsStr = regularLogs.join('\\n').trim(); |
|
|
|
|
|
if (uvLogsStr) { |
|
|
html+='<div class="uv-install-logs">'; |
|
|
html+='<div class="uv-logs-header" onclick="toggleUvLogs(this)">▶ UV Install Logs</div>'; |
|
|
html+='<div class="uv-logs-content" style="display: none;">'+escapeHtml(uvLogsStr)+'</div>'; |
|
|
html+='</div>'; |
|
|
} |
|
|
if (regularLogsStr) { |
|
|
html+='<div class="cell-stderr">'+escapeHtml(regularLogsStr)+'</div>'; |
|
|
} |
|
|
} |
|
|
} |
|
|
output.innerHTML=html; |
|
|
} |
|
|
if(btn){btn.textContent='▶ run';btn.disabled=false;} |
|
|
}).catch(e=>{ |
|
|
console.error('Run failed:',e); |
|
|
if(output){output.classList.remove('output-stale');} |
|
|
if(btn){btn.textContent='▶ run';btn.disabled=false;} |
|
|
}); |
|
|
} |
|
|
|
|
|
function copyCell(cellId){ |
|
|
console.log('copyCell called with cellId:', cellId); |
|
|
|
|
|
|
|
|
let codeElement = document.querySelector('#code-'+cellId+' code'); |
|
|
if (!codeElement) { |
|
|
codeElement = document.querySelector('#code-'+cellId+' pre code'); |
|
|
} |
|
|
if (!codeElement) { |
|
|
codeElement = document.querySelector('#code-'+cellId+' .highlight code'); |
|
|
} |
|
|
if (!codeElement) { |
|
|
|
|
|
const codeDiv = document.getElementById('code-'+cellId); |
|
|
if (codeDiv) { |
|
|
codeElement = codeDiv.querySelector('code'); |
|
|
} |
|
|
} |
|
|
|
|
|
const btn = document.querySelector('.copy-btn[onclick*="'+cellId+'"]'); |
|
|
|
|
|
console.log('Found codeElement:', codeElement); |
|
|
console.log('Found btn:', btn); |
|
|
console.log('Code div structure:', document.getElementById('code-'+cellId)); |
|
|
|
|
|
if (!codeElement) { |
|
|
console.error('Code element not found for cell:', cellId); |
|
|
|
|
|
const codeDiv = document.getElementById('code-'+cellId); |
|
|
if (codeDiv) { |
|
|
console.log('Code div HTML:', codeDiv.innerHTML); |
|
|
} |
|
|
return; |
|
|
} |
|
|
if (!btn) { |
|
|
console.error('Copy button not found for cell:', cellId); |
|
|
return; |
|
|
} |
|
|
|
|
|
const codeText = codeElement.textContent; |
|
|
console.log('Code text to copy:', codeText ? codeText.substring(0, 50) + '...' : 'empty'); |
|
|
|
|
|
if (navigator.clipboard && navigator.clipboard.writeText) { |
|
|
navigator.clipboard.writeText(codeText).then(function() { |
|
|
console.log('Clipboard copy successful'); |
|
|
btn.textContent = '✓ Copied!'; |
|
|
btn.classList.add('copied'); |
|
|
setTimeout(function() { |
|
|
btn.textContent = 'Copy'; |
|
|
btn.classList.remove('copied'); |
|
|
}, 2000); |
|
|
}).catch(function(err) { |
|
|
console.warn('Clipboard copy failed:', err); |
|
|
fallbackCopy(); |
|
|
}); |
|
|
} else { |
|
|
console.log('Using fallback copy method'); |
|
|
fallbackCopy(); |
|
|
} |
|
|
|
|
|
function fallbackCopy() { |
|
|
const textarea = document.createElement('textarea'); |
|
|
textarea.value = codeText; |
|
|
textarea.style.position = 'absolute'; |
|
|
textarea.style.left = '-9999px'; |
|
|
document.body.appendChild(textarea); |
|
|
textarea.select(); |
|
|
try { |
|
|
const success = document.execCommand('copy'); |
|
|
console.log('Fallback copy success:', success); |
|
|
btn.textContent = '✓ Copied!'; |
|
|
btn.classList.add('copied'); |
|
|
setTimeout(function() { |
|
|
btn.textContent = 'Copy'; |
|
|
btn.classList.remove('copied'); |
|
|
}, 2000); |
|
|
} catch (err) { |
|
|
console.error('Fallback copy failed:', err); |
|
|
btn.textContent = 'Copy failed'; |
|
|
setTimeout(function() { |
|
|
btn.textContent = 'Copy'; |
|
|
}, 2000); |
|
|
} |
|
|
document.body.removeChild(textarea); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
(function(){ |
|
|
if (!('EventSource' in window)) { |
|
|
console.warn('SSE not supported in this browser'); |
|
|
return; |
|
|
} |
|
|
let source = new EventSource('/events'); |
|
|
let isOpen = false; |
|
|
source.onopen = function(){ isOpen = true; console.log('SSE connected'); }; |
|
|
source.onmessage = function(e){ |
|
|
const msg=(e.data||'').trim(); if(!msg) return; |
|
|
console.log('SSE message:', msg); |
|
|
if (msg==='reload' || msg==='incremental') { location.reload(); } |
|
|
|
|
|
}; |
|
|
source.onerror = function(e){ |
|
|
|
|
|
if (isOpen) console.warn('SSE error after open, retrying...', e); |
|
|
}; |
|
|
window.addEventListener('beforeunload', function(){ try{source.close();}catch(_){} }); |
|
|
})(); |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
|
|
captureInitialCellStates(); |
|
|
|
|
|
updateThemeIcon(); |
|
|
updateUiMenu(); |
|
|
updateUiDebug(); |
|
|
const widgetsEnabled = (document.documentElement.getAttribute('data-widgets') || 'on') === 'on'; |
|
|
if (widgetsEnabled) { |
|
|
initMinimap(); |
|
|
initFileExplorer(); |
|
|
initTools(); |
|
|
initOverlay(); |
|
|
initStatusWidget(); |
|
|
initializeWidgetVisibility(); |
|
|
layoutWidgetsStackedBottomRight(); |
|
|
window.addEventListener('resize', layoutWidgetsStackedBottomRight); |
|
|
} |
|
|
|
|
|
|
|
|
applyLocationFromUrl(); |
|
|
updateStateIndicator(); |
|
|
|
|
|
|
|
|
const url = new URL(window.location.href); |
|
|
const encodedCellStates = url.searchParams.get('cells'); |
|
|
if (encodedCellStates) { |
|
|
console.log('Applying cell states from URL...'); |
|
|
const cellStates = decodeCellStatesFromUrl(encodedCellStates); |
|
|
|
|
|
|
|
|
requestAnimationFrame(() => { |
|
|
applyCellStatesFromUrl(cellStates); |
|
|
|
|
|
|
|
|
if (typeof _isInitializing !== 'undefined') { |
|
|
_isInitializing = false; |
|
|
} |
|
|
}); |
|
|
} else { |
|
|
|
|
|
if (typeof _isInitializing !== 'undefined') { |
|
|
requestAnimationFrame(() => { |
|
|
_isInitializing = false; |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('mousedown', onLineNumberMouseDown); |
|
|
document.addEventListener('mousemove', onDocMouseMove); |
|
|
document.addEventListener('mouseup', onDocMouseUp); |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', function(e) { |
|
|
if (e.key === 'Escape' || e.keyCode === 27) { |
|
|
const currentTool = document.body.dataset.tool; |
|
|
if (currentTool && currentTool !== 'none') { |
|
|
|
|
|
window.setActiveTool('none'); |
|
|
} |
|
|
|
|
|
clearSelection(true); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
function updateStateIndicator() { |
|
|
try { |
|
|
const el = document.getElementById('status-widget'); |
|
|
if (!el) return; |
|
|
const tool = document.body.dataset.tool || 'none'; |
|
|
if (tool && tool !== 'none') { |
|
|
el.textContent = `tool: ${tool} — Esc`; |
|
|
return; |
|
|
} |
|
|
if (_selection) { |
|
|
const t = _selection.a === _selection.b ? `L${_selection.a}` : `L${_selection.a}-${_selection.b}`; |
|
|
el.textContent = `selected: ${t} — Esc`; |
|
|
return; |
|
|
} |
|
|
el.textContent = 'ready — Esc'; |
|
|
} catch (_) {} |
|
|
} |
|
|
</script> |
|
|
</head> |
|
|
|
|
|
|
|
|
<body> |
|
|
<div class="controls"> |
|
|
<div class="controls-buttons"> |
|
|
<div class="theme-toggle" onclick="toggleTheme()">light</div> |
|
|
<div class="reset-toggle" onclick="resetLayout()">reset</div> |
|
|
<div class="menu-button" onclick="toggleMenu()"> |
|
|
menu ▼ |
|
|
<div class="menu-dropdown"> |
|
|
<div class="menu-item" onclick="setUiTheme('default')"> |
|
|
<span class="menu-checkbox" id="checkbox-ui-default">☑</span> Theme: default |
|
|
</div> |
|
|
<div class="menu-item" onclick="setUiTheme('none')"> |
|
|
<span class="menu-checkbox" id="checkbox-ui-none">☐</span> Theme: none |
|
|
</div> |
|
|
<div class="menu-item" onclick="setUiTheme('monocolor')"> |
|
|
<span class="menu-checkbox" id="checkbox-ui-monocolor">☐</span> Theme: monocolor |
|
|
</div> |
|
|
<div class="menu-item" onclick="toggleWidget('tools')"> |
|
|
<span class="menu-checkbox" id="checkbox-tools">☐</span> Tools |
|
|
</div> |
|
|
<div class="menu-item" onclick="toggleWidget('file-explorer')"> |
|
|
<span class="menu-checkbox" id="checkbox-file-explorer">☐</span> File Explorer |
|
|
</div> |
|
|
<div class="menu-item" onclick="toggleWidget('minimap')"> |
|
|
<span class="menu-checkbox" id="checkbox-minimap">☐</span> Table of Contents |
|
|
</div> |
|
|
<div class="menu-item" onclick="toggleWidget('status')"> |
|
|
<span class="menu-checkbox" id="checkbox-status">☑</span> Status Indicator |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="system-info"> |
|
|
<div class="system-info-header">Generated on:</div> |
|
|
<div class="system-info-content"> |
|
|
Linux x86_64 | Linux-6.12.40-64.114.amzn2023.x86_64-x86_64-with-glibc2.36 |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="main-content"> |
|
|
<div class="cell" id="cell-setup"> |
|
|
<div class="cell-header"> |
|
|
<span class="collapse-indicators"> |
|
|
<span onclick="toggleCode('setup')" style="cursor: pointer;">▼ code</span> |
|
|
<span onclick="toggleOutput('setup')" style="cursor: pointer;">▼ output</span> |
|
|
<span id="uv-indicator-setup" onclick="toggleUvLogsFromHeader('setup')" style="cursor: pointer;">▶ uv-logs</span> |
|
|
</span> | |
|
|
Cell: setup | 132.82s |
|
|
| <button class="run-btn" onclick="runCell('setup')">▶ run</button> |
|
|
<button class="copy-btn" onclick="copyCell('setup')">Copy</button> |
|
|
<a href="cells/setup.py" target="_blank" class="raw-btn">Raw</a> |
|
|
</div> |
|
|
<div id="code-setup" class="cell-code" data-lines="116"> |
|
|
<div class="highlight-with-lines"> |
|
|
<div class="line-numbers" id="lines-setup"> |
|
|
<a class="line-number" data-cell="setup" data-line="1" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 1, true);">1</a> |
|
|
<a class="line-number" data-cell="setup" data-line="2" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 2, true);">2</a> |
|
|
<a class="line-number" data-cell="setup" data-line="3" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 3, true);">3</a> |
|
|
<a class="line-number" data-cell="setup" data-line="4" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 4, true);">4</a> |
|
|
<a class="line-number" data-cell="setup" data-line="5" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 5, true);">5</a> |
|
|
<a class="line-number" data-cell="setup" data-line="6" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 6, true);">6</a> |
|
|
<a class="line-number" data-cell="setup" data-line="7" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 7, true);">7</a> |
|
|
<a class="line-number" data-cell="setup" data-line="8" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 8, true);">8</a> |
|
|
<a class="line-number" data-cell="setup" data-line="9" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 9, true);">9</a> |
|
|
<a class="line-number" data-cell="setup" data-line="10" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 10, true);">10</a> |
|
|
<a class="line-number" data-cell="setup" data-line="11" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 11, true);">11</a> |
|
|
<a class="line-number" data-cell="setup" data-line="12" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 12, true);">12</a> |
|
|
<a class="line-number" data-cell="setup" data-line="13" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 13, true);">13</a> |
|
|
<a class="line-number" data-cell="setup" data-line="14" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 14, true);">14</a> |
|
|
<a class="line-number" data-cell="setup" data-line="15" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 15, true);">15</a> |
|
|
<a class="line-number" data-cell="setup" data-line="16" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 16, true);">16</a> |
|
|
<a class="line-number" data-cell="setup" data-line="17" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 17, true);">17</a> |
|
|
<a class="line-number" data-cell="setup" data-line="18" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 18, true);">18</a> |
|
|
<a class="line-number" data-cell="setup" data-line="19" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 19, true);">19</a> |
|
|
<a class="line-number" data-cell="setup" data-line="20" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 20, true);">20</a> |
|
|
<a class="line-number" data-cell="setup" data-line="21" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 21, true);">21</a> |
|
|
<a class="line-number" data-cell="setup" data-line="22" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 22, true);">22</a> |
|
|
<a class="line-number" data-cell="setup" data-line="23" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 23, true);">23</a> |
|
|
<a class="line-number" data-cell="setup" data-line="24" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 24, true);">24</a> |
|
|
<a class="line-number" data-cell="setup" data-line="25" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 25, true);">25</a> |
|
|
<a class="line-number" data-cell="setup" data-line="26" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 26, true);">26</a> |
|
|
<a class="line-number" data-cell="setup" data-line="27" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 27, true);">27</a> |
|
|
<a class="line-number" data-cell="setup" data-line="28" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 28, true);">28</a> |
|
|
<a class="line-number" data-cell="setup" data-line="29" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 29, true);">29</a> |
|
|
<a class="line-number" data-cell="setup" data-line="30" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 30, true);">30</a> |
|
|
<a class="line-number" data-cell="setup" data-line="31" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 31, true);">31</a> |
|
|
<a class="line-number" data-cell="setup" data-line="32" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 32, true);">32</a> |
|
|
<a class="line-number" data-cell="setup" data-line="33" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 33, true);">33</a> |
|
|
<a class="line-number" data-cell="setup" data-line="34" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 34, true);">34</a> |
|
|
<a class="line-number" data-cell="setup" data-line="35" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 35, true);">35</a> |
|
|
<a class="line-number" data-cell="setup" data-line="36" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 36, true);">36</a> |
|
|
<a class="line-number" data-cell="setup" data-line="37" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 37, true);">37</a> |
|
|
<a class="line-number" data-cell="setup" data-line="38" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 38, true);">38</a> |
|
|
<a class="line-number" data-cell="setup" data-line="39" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 39, true);">39</a> |
|
|
<a class="line-number" data-cell="setup" data-line="40" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 40, true);">40</a> |
|
|
<a class="line-number" data-cell="setup" data-line="41" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 41, true);">41</a> |
|
|
<a class="line-number" data-cell="setup" data-line="42" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 42, true);">42</a> |
|
|
<a class="line-number" data-cell="setup" data-line="43" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 43, true);">43</a> |
|
|
<a class="line-number" data-cell="setup" data-line="44" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 44, true);">44</a> |
|
|
<a class="line-number" data-cell="setup" data-line="45" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 45, true);">45</a> |
|
|
<a class="line-number" data-cell="setup" data-line="46" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 46, true);">46</a> |
|
|
<a class="line-number" data-cell="setup" data-line="47" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 47, true);">47</a> |
|
|
<a class="line-number" data-cell="setup" data-line="48" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 48, true);">48</a> |
|
|
<a class="line-number" data-cell="setup" data-line="49" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 49, true);">49</a> |
|
|
<a class="line-number" data-cell="setup" data-line="50" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 50, true);">50</a> |
|
|
<a class="line-number" data-cell="setup" data-line="51" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 51, true);">51</a> |
|
|
<a class="line-number" data-cell="setup" data-line="52" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 52, true);">52</a> |
|
|
<a class="line-number" data-cell="setup" data-line="53" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 53, true);">53</a> |
|
|
<a class="line-number" data-cell="setup" data-line="54" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 54, true);">54</a> |
|
|
<a class="line-number" data-cell="setup" data-line="55" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 55, true);">55</a> |
|
|
<a class="line-number" data-cell="setup" data-line="56" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 56, true);">56</a> |
|
|
<a class="line-number" data-cell="setup" data-line="57" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 57, true);">57</a> |
|
|
<a class="line-number" data-cell="setup" data-line="58" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 58, true);">58</a> |
|
|
<a class="line-number" data-cell="setup" data-line="59" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 59, true);">59</a> |
|
|
<a class="line-number" data-cell="setup" data-line="60" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 60, true);">60</a> |
|
|
<a class="line-number" data-cell="setup" data-line="61" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 61, true);">61</a> |
|
|
<a class="line-number" data-cell="setup" data-line="62" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 62, true);">62</a> |
|
|
<a class="line-number" data-cell="setup" data-line="63" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 63, true);">63</a> |
|
|
<a class="line-number" data-cell="setup" data-line="64" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 64, true);">64</a> |
|
|
<a class="line-number" data-cell="setup" data-line="65" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 65, true);">65</a> |
|
|
<a class="line-number" data-cell="setup" data-line="66" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 66, true);">66</a> |
|
|
<a class="line-number" data-cell="setup" data-line="67" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 67, true);">67</a> |
|
|
<a class="line-number" data-cell="setup" data-line="68" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 68, true);">68</a> |
|
|
<a class="line-number" data-cell="setup" data-line="69" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 69, true);">69</a> |
|
|
<a class="line-number" data-cell="setup" data-line="70" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 70, true);">70</a> |
|
|
<a class="line-number" data-cell="setup" data-line="71" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 71, true);">71</a> |
|
|
<a class="line-number" data-cell="setup" data-line="72" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 72, true);">72</a> |
|
|
<a class="line-number" data-cell="setup" data-line="73" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 73, true);">73</a> |
|
|
<a class="line-number" data-cell="setup" data-line="74" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 74, true);">74</a> |
|
|
<a class="line-number" data-cell="setup" data-line="75" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 75, true);">75</a> |
|
|
<a class="line-number" data-cell="setup" data-line="76" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 76, true);">76</a> |
|
|
<a class="line-number" data-cell="setup" data-line="77" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 77, true);">77</a> |
|
|
<a class="line-number" data-cell="setup" data-line="78" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 78, true);">78</a> |
|
|
<a class="line-number" data-cell="setup" data-line="79" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 79, true);">79</a> |
|
|
<a class="line-number" data-cell="setup" data-line="80" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 80, true);">80</a> |
|
|
<a class="line-number" data-cell="setup" data-line="81" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 81, true);">81</a> |
|
|
<a class="line-number" data-cell="setup" data-line="82" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 82, true);">82</a> |
|
|
<a class="line-number" data-cell="setup" data-line="83" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 83, true);">83</a> |
|
|
<a class="line-number" data-cell="setup" data-line="84" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 84, true);">84</a> |
|
|
<a class="line-number" data-cell="setup" data-line="85" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 85, true);">85</a> |
|
|
<a class="line-number" data-cell="setup" data-line="86" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 86, true);">86</a> |
|
|
<a class="line-number" data-cell="setup" data-line="87" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 87, true);">87</a> |
|
|
<a class="line-number" data-cell="setup" data-line="88" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 88, true);">88</a> |
|
|
<a class="line-number" data-cell="setup" data-line="89" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 89, true);">89</a> |
|
|
<a class="line-number" data-cell="setup" data-line="90" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 90, true);">90</a> |
|
|
<a class="line-number" data-cell="setup" data-line="91" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 91, true);">91</a> |
|
|
<a class="line-number" data-cell="setup" data-line="92" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 92, true);">92</a> |
|
|
<a class="line-number" data-cell="setup" data-line="93" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 93, true);">93</a> |
|
|
<a class="line-number" data-cell="setup" data-line="94" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 94, true);">94</a> |
|
|
<a class="line-number" data-cell="setup" data-line="95" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 95, true);">95</a> |
|
|
<a class="line-number" data-cell="setup" data-line="96" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 96, true);">96</a> |
|
|
<a class="line-number" data-cell="setup" data-line="97" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 97, true);">97</a> |
|
|
<a class="line-number" data-cell="setup" data-line="98" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 98, true);">98</a> |
|
|
<a class="line-number" data-cell="setup" data-line="99" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 99, true);">99</a> |
|
|
<a class="line-number" data-cell="setup" data-line="100" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 100, true);">100</a> |
|
|
<a class="line-number" data-cell="setup" data-line="101" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 101, true);">101</a> |
|
|
<a class="line-number" data-cell="setup" data-line="102" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 102, true);">102</a> |
|
|
<a class="line-number" data-cell="setup" data-line="103" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 103, true);">103</a> |
|
|
<a class="line-number" data-cell="setup" data-line="104" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 104, true);">104</a> |
|
|
<a class="line-number" data-cell="setup" data-line="105" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 105, true);">105</a> |
|
|
<a class="line-number" data-cell="setup" data-line="106" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 106, true);">106</a> |
|
|
<a class="line-number" data-cell="setup" data-line="107" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 107, true);">107</a> |
|
|
<a class="line-number" data-cell="setup" data-line="108" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 108, true);">108</a> |
|
|
<a class="line-number" data-cell="setup" data-line="109" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 109, true);">109</a> |
|
|
<a class="line-number" data-cell="setup" data-line="110" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 110, true);">110</a> |
|
|
<a class="line-number" data-cell="setup" data-line="111" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 111, true);">111</a> |
|
|
<a class="line-number" data-cell="setup" data-line="112" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 112, true);">112</a> |
|
|
<a class="line-number" data-cell="setup" data-line="113" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 113, true);">113</a> |
|
|
<a class="line-number" data-cell="setup" data-line="114" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 114, true);">114</a> |
|
|
<a class="line-number" data-cell="setup" data-line="115" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 115, true);">115</a> |
|
|
<a class="line-number" data-cell="setup" data-line="116" href="#cell-setup" onclick="event.preventDefault(); selectCellLine('setup', 116, true);">116</a> |
|
|
</div> |
|
|
<div class="code-wrap"> |
|
|
<div class="highlight"><pre><span></span><span class="c1"># /// script</span> |
|
|
<span class="c1"># requires-python = ">=3.12"</span> |
|
|
<span class="c1"># dependencies = [</span> |
|
|
<span class="c1"># "accelerate>=1.10.1",</span> |
|
|
<span class="c1"># "torch>=2.7.0",</span> |
|
|
<span class="c1"># "kernels==0.10.0",</span> |
|
|
<span class="c1"># "transformers@https://github.com/huggingface/transformers.git",</span> |
|
|
<span class="c1"># "ipdb>=0.13.13",</span> |
|
|
<span class="c1"># "matplotlib>=3.7.2",</span> |
|
|
<span class="c1"># "numpy>=1.24.3",</span> |
|
|
<span class="c1"># ]</span> |
|
|
<span class="c1"># ///</span> |
|
|
|
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">torch</span> |
|
|
<span class="kn">from</span><span class="w"> </span><span class="nn">transformers</span><span class="w"> </span><span class="kn">import</span> <span class="n">GptOssForCausalLM</span><span class="p">,</span> <span class="n">PreTrainedTokenizerFast</span><span class="p">,</span> <span class="n">Mxfp4Config</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">time</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">torch.nn</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">nn</span> |
|
|
<span class="kn">from</span><span class="w"> </span><span class="nn">kernels</span><span class="w"> </span><span class="kn">import</span> <span class="n">register_kernel_mapping</span><span class="p">,</span> <span class="n">Mode</span><span class="p">,</span> <span class="n">LayerRepository</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">sys</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">torch.profiler</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">gc</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">logging</span> |
|
|
|
|
|
<span class="c1"># set to debug logging</span> |
|
|
<span class="n">logging</span><span class="o">.</span><span class="n">basicConfig</span><span class="p">(</span><span class="n">level</span><span class="o">=</span><span class="n">logging</span><span class="o">.</span><span class="n">INFO</span><span class="p">)</span> |
|
|
|
|
|
<span class="k">def</span><span class="w"> </span><span class="nf">reset_peak_memory_stats</span><span class="p">():</span> |
|
|
<span class="w"> </span><span class="sd">"""Clear CUDA cache and reset memory allocation counters."""</span> |
|
|
<span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">empty_cache</span><span class="p">()</span> |
|
|
<span class="k">if</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">is_available</span><span class="p">():</span> |
|
|
<span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">reset_peak_memory_stats</span><span class="p">()</span> |
|
|
<span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span> |
|
|
|
|
|
<span class="k">def</span><span class="w"> </span><span class="nf">get_memory_stats</span><span class="p">():</span> |
|
|
<span class="w"> </span><span class="sd">"""Get current and peak CUDA memory usage."""</span> |
|
|
<span class="k">if</span> <span class="ow">not</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">is_available</span><span class="p">():</span> |
|
|
<span class="k">return</span> <span class="p">{</span><span class="s2">"allocated_gb"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">"peak_gb"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">"reserved_gb"</span><span class="p">:</span> <span class="mi">0</span><span class="p">}</span> |
|
|
<span class="k">return</span> <span class="p">{</span> |
|
|
<span class="s2">"allocated_gb"</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">memory_allocated</span><span class="p">()</span> <span class="o">/</span> <span class="mf">1e9</span><span class="p">,</span> |
|
|
<span class="s2">"peak_gb"</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">max_memory_allocated</span><span class="p">()</span> <span class="o">/</span> <span class="mf">1e9</span><span class="p">,</span> |
|
|
<span class="s2">"reserved_gb"</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">memory_reserved</span><span class="p">()</span> <span class="o">/</span> <span class="mf">1e9</span><span class="p">,</span> |
|
|
<span class="p">}</span> |
|
|
|
|
|
<span class="k">def</span><span class="w"> </span><span class="nf">override_kernel_layer_name</span><span class="p">(</span><span class="n">cls_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> <span class="o">-></span> <span class="nb">bool</span><span class="p">:</span> |
|
|
<span class="w"> </span><span class="sd">"""Helper to dynamically override the kernel_layer_name in a model class."""</span> |
|
|
<span class="k">for</span> <span class="n">mod</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">modules</span><span class="o">.</span><span class="n">values</span><span class="p">():</span> |
|
|
<span class="k">if</span> <span class="n">mod</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span> |
|
|
<span class="k">continue</span> |
|
|
<span class="n">obj</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">mod</span><span class="p">,</span> <span class="n">cls_name</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span> |
|
|
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="nb">type</span><span class="p">)</span> <span class="ow">and</span> <span class="nb">issubclass</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">nn</span><span class="o">.</span><span class="n">Module</span><span class="p">):</span> |
|
|
<span class="nb">setattr</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="s2">"kernel_layer_name"</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> |
|
|
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Overrode </span><span class="si">{</span><span class="n">cls_name</span><span class="si">}</span><span class="s2">.kernel_layer_name to </span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> |
|
|
<span class="k">return</span> <span class="kc">True</span> |
|
|
<span class="k">return</span> <span class="kc">False</span> |
|
|
|
|
|
|
|
|
<span class="c1"># Init the model the normal way</span> |
|
|
<span class="n">model_id</span> <span class="o">=</span> <span class="s2">"openai/gpt-oss-20b"</span> |
|
|
<span class="n">tokenizer</span> <span class="o">=</span> <span class="n">PreTrainedTokenizerFast</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_id</span><span class="p">)</span> |
|
|
<span class="n">quantization_config</span> <span class="o">=</span> <span class="n">Mxfp4Config</span><span class="p">(</span><span class="n">dequantize</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span> |
|
|
|
|
|
|
|
|
<span class="kn">from</span><span class="w"> </span><span class="nn">kernels</span><span class="w"> </span><span class="kn">import</span> <span class="n">replace_kernel_forward_from_hub</span><span class="p">,</span> <span class="n">register_kernel_mapping</span><span class="p">,</span> <span class="n">LayerRepository</span><span class="p">,</span> <span class="n">Mode</span> |
|
|
|
|
|
<span class="kn">from</span><span class="w"> </span><span class="nn">transformers.models.gpt_oss.modeling_gpt_oss</span><span class="w"> </span><span class="kn">import</span> <span class="n">GptOssMLP</span><span class="p">,</span> <span class="n">GptOssRMSNorm</span> |
|
|
|
|
|
<span class="n">replace_kernel_forward_from_hub</span><span class="p">(</span><span class="n">GptOssMLP</span><span class="p">,</span> <span class="s2">"Yamoe"</span><span class="p">)</span> <span class="c1"># direct, type-safe</span> |
|
|
<span class="n">replace_kernel_forward_from_hub</span><span class="p">(</span><span class="n">GptOssRMSNorm</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span> <span class="c1"># direct, type-safe</span> |
|
|
<span class="n">custom_mapping</span> <span class="o">=</span> <span class="p">{</span> |
|
|
<span class="s2">"Yamoe"</span><span class="p">:</span> <span class="p">{</span> |
|
|
<span class="s2">"cuda"</span><span class="p">:</span> <span class="p">{</span> |
|
|
<span class="n">Mode</span><span class="o">.</span><span class="n">INFERENCE</span><span class="p">:</span> <span class="n">LayerRepository</span><span class="p">(</span> |
|
|
<span class="n">repo_id</span><span class="o">=</span><span class="s2">"drbh/yamoe"</span><span class="p">,</span> |
|
|
<span class="n">layer_name</span><span class="o">=</span><span class="s2">"Yamoe"</span><span class="p">,</span> |
|
|
<span class="n">revision</span><span class="o">=</span><span class="s2">"v0.3.0"</span><span class="p">,</span> |
|
|
<span class="p">)</span> |
|
|
<span class="p">}</span> |
|
|
<span class="p">}</span> |
|
|
<span class="p">}</span> |
|
|
<span class="n">register_kernel_mapping</span><span class="p">(</span><span class="n">custom_mapping</span><span class="p">)</span> |
|
|
|
|
|
|
|
|
<span class="n">model</span> <span class="o">=</span> <span class="n">GptOssForCausalLM</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span> |
|
|
<span class="n">model_id</span><span class="p">,</span> |
|
|
<span class="n">dtype</span><span class="o">=</span><span class="s2">"bfloat16"</span><span class="p">,</span> |
|
|
<span class="n">device_map</span><span class="o">=</span><span class="s2">"auto"</span><span class="p">,</span> |
|
|
<span class="n">use_kernels</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> |
|
|
<span class="n">quantization_config</span><span class="o">=</span><span class="n">quantization_config</span><span class="p">,</span> |
|
|
<span class="p">)</span><span class="o">.</span><span class="n">eval</span><span class="p">()</span> |
|
|
|
|
|
<span class="n">messages</span> <span class="o">=</span> <span class="p">[</span> |
|
|
<span class="p">{</span><span class="s2">"role"</span><span class="p">:</span> <span class="s2">"system"</span><span class="p">,</span> <span class="s2">"content"</span><span class="p">:</span> <span class="s2">"What is Tensor Parallelism?"</span><span class="p">},</span> |
|
|
<span class="p">]</span> |
|
|
|
|
|
<span class="n">inputs</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="o">.</span><span class="n">apply_chat_template</span><span class="p">(</span> |
|
|
<span class="n">messages</span><span class="p">,</span> |
|
|
<span class="n">add_generation_prompt</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> |
|
|
<span class="n">return_tensors</span><span class="o">=</span><span class="s2">"pt"</span><span class="p">,</span> |
|
|
<span class="n">return_dict</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> |
|
|
<span class="n">reasoning_effort</span><span class="o">=</span><span class="s2">"low"</span><span class="p">,</span> |
|
|
<span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="s2">"cuda"</span><span class="p">)</span> |
|
|
|
|
|
<span class="n">max_tokens</span> <span class="o">=</span> <span class="mi">512</span> |
|
|
|
|
|
<span class="k">with</span> <span class="n">torch</span><span class="o">.</span><span class="n">inference_mode</span><span class="p">():</span> |
|
|
<span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span> |
|
|
<span class="n">generated</span> <span class="o">=</span> <span class="n">model</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span> |
|
|
<span class="o">**</span><span class="n">inputs</span><span class="p">,</span> |
|
|
<span class="n">max_new_tokens</span><span class="o">=</span><span class="n">max_tokens</span><span class="p">,</span> |
|
|
<span class="n">do_sample</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> |
|
|
<span class="n">temperature</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> |
|
|
<span class="p">)</span> |
|
|
<span class="n">end_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span> |
|
|
|
|
|
<span class="nb">print</span><span class="p">(</span><span class="n">tokenizer</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="n">generated</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">skip_special_tokens</span><span class="o">=</span><span class="kc">False</span><span class="p">))</span> |
|
|
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Generation took </span><span class="si">{</span><span class="n">end_time</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">start_time</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2"> seconds"</span><span class="p">)</span> |
|
|
</pre></div> |
|
|
|
|
|
<div class="code-line-highlight" id="line-highlight-setup"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div id="output-setup" class="cell-output"> |
|
|
<div class="cell-stdout"><|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI. |
|
|
Knowledge cutoff: 2024-06 |
|
|
Current date: 2025-09-23 |
|
|
|
|
|
Reasoning: low |
|
|
|
|
|
# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions |
|
|
|
|
|
What is Tensor Parallelism? |
|
|
|
|
|
<|end|><|start|>assistant<|channel|>analysis<|message|>We need to explain what Tensor Parallelism is. It's a concept in distributed training of large language models. It refers to splitting the weight matrices (tensors) across multiple devices, so each device holds a slice of the matrix. During forward/backward passes, each device computes partial results and then they are aggregated. It's used to scale up models beyond single device memory. Also mention pipeline parallelism, data parallelism. Provide details: e.g., for a linear layer weight matrix W of shape (out_features, in_features), we can split along out_features dimension across devices. Each device computes its part of the output. Then gather. Similarly for attention QKV projections. Provide example: GPT-3 uses tensor parallelism. Also mention frameworks: Megatron-LM, DeepSpeed, etc. Provide pros/cons. Provide typical implementation: using torch.distributed.all_reduce, gather, etc. Provide code snippet. Also mention that it's different from data parallelism. Provide explanation of how it works in practice. Provide mention of "tensor model parallelism" vs "tensor parallelism" synonyms. Provide mention of "tensor parallelism" in context of "DeepSpeed ZeRO Stage 3" or "Megatron-LM". Provide mention of "tensor parallelism" as part of "model parallelism" to reduce memory usage. Provide mention of "tensor parallelism" as "splitting weight matrices across GPUs" and "communication overhead". |
|
|
|
|
|
Also mention that it's used for large transformer models like GPT-3, LLaMA, etc. Provide mention of "tensor parallelism" in the context of "DeepSpeed's ZeRO-Offload" or "ZeRO-3". |
|
|
|
|
|
Also mention that "tensor parallelism" can be combined with "pipeline parallelism" and "data parallelism" to achieve full scaling. |
|
|
|
|
|
Also mention that "tensor parallelism" can be implemented by splitting the weight matrix along the output dimension, performing local matrix multiplication, then all-reduce to sum partial outputs. |
|
|
|
|
|
Also mention that "tensor parallelism" can be used for linear layers, self-attention, feed-forward networks, etc. |
|
|
|
|
|
Also mention that "tensor parallelism" can be used for "embedding tables" by sharding them across devices. |
|
|
|
|
|
Also mention that "tensor parallelism" can be used for "attention heads" by splitting across heads. |
|
|
|
|
|
Also mention that "tensor parallelism" can be used for "parameter sharding". |
|
|
|
|
|
Also mention that "tensor parallelism" can be used for "model parallelism" to reduce memory usage. |
|
|
|
|
|
|
|
|
Generation took 51.90 seconds |
|
|
</div> |
|
|
<div class="uv-install-logs" id="uv-logs-setup"> |
|
|
<div class="uv-logs-header" onclick="toggleUvLogs(this)">▶ UV Install Logs</div> |
|
|
<div class="uv-logs-content" style="display: none;"> |
|
|
Downloading cpython-3.13.7-linux-x86_64-gnu (download) (32.0MiB) |
|
|
Downloading cpython-3.13.7-linux-x86_64-gnu (download) |
|
|
Updating https://github.com/huggingface/transformers.git (HEAD) |
|
|
Updated https://github.com/huggingface/transformers.git (99b0995138c17ef953959c70f35cb2bdc41111a2) |
|
|
Building transformers @ git+https://github.com/huggingface/transformers.git@99b0995138c17ef953959c70f35cb2bdc41111a2 |
|
|
Downloading nvidia-cusolver-cu12 (255.1MiB) |
|
|
Downloading nvidia-cuda-nvrtc-cu12 (84.0MiB) |
|
|
Downloading nvidia-cuda-cupti-cu12 (9.8MiB) |
|
|
Downloading sympy (6.0MiB) |
|
|
Downloading jedi (1.5MiB) |
|
|
Downloading fonttools (4.7MiB) |
|
|
Downloading pillow (6.3MiB) |
|
|
Downloading nvidia-cusparse-cu12 (274.9MiB) |
|
|
Downloading nvidia-curand-cu12 (60.7MiB) |
|
|
Downloading numpy (15.9MiB) |
|
|
Downloading nvidia-cufft-cu12 (184.2MiB) |
|
|
Downloading matplotlib (8.3MiB) |
|
|
Downloading pygments (1.2MiB) |
|
|
Downloading nvidia-cublas-cu12 (566.8MiB) |
|
|
Downloading kiwisolver (1.4MiB) |
|
|
Downloading nvidia-nccl-cu12 (307.4MiB) |
|
|
Downloading nvidia-cusparselt-cu12 (273.9MiB) |
|
|
Downloading nvidia-nvjitlink-cu12 (37.4MiB) |
|
|
Downloading networkx (1.9MiB) |
|
|
Downloading hf-xet (3.0MiB) |
|
|
Downloading nvidia-cudnn-cu12 (674.0MiB) |
|
|
Downloading torch (846.8MiB) |
|
|
Downloading tokenizers (3.1MiB) |
|
|
Downloading nvidia-cufile-cu12 (1.1MiB) |
|
|
Downloading triton (148.4MiB) |
|
|
Downloading nvidia-cufile-cu12 |
|
|
Downloading kiwisolver |
|
|
Downloading pygments |
|
|
Downloading hf-xet |
|
|
Downloading tokenizers |
|
|
Downloading networkx |
|
|
Downloading fonttools |
|
|
Downloading pillow |
|
|
Downloading matplotlib |
|
|
Downloading nvidia-cuda-cupti-cu12 |
|
|
Downloading numpy |
|
|
Downloading sympy |
|
|
Downloading nvidia-nvjitlink-cu12 |
|
|
Built transformers @ git+https://github.com/huggingface/transformers.git@99b0995138c17ef953959c70f35cb2bdc41111a2 |
|
|
Downloading jedi |
|
|
Downloading nvidia-curand-cu12 |
|
|
Downloading nvidia-cuda-nvrtc-cu12 |
|
|
Downloading triton |
|
|
Downloading nvidia-cufft-cu12 |
|
|
Downloading nvidia-cusolver-cu12 |
|
|
Downloading nvidia-cusparselt-cu12 |
|
|
Downloading nvidia-cusparse-cu12 |
|
|
Downloading nvidia-nccl-cu12 |
|
|
Downloading nvidia-cublas-cu12 |
|
|
Downloading nvidia-cudnn-cu12 |
|
|
Downloading torch |
|
|
Installed 69 packages in 539ms |
|
|
</div> |
|
|
</div> |
|
|
<div class="cell-stderr">Fetching 3 files: 0%| | 0/3 [00:00<?, ?it/s] |
|
|
Fetching 3 files: 33%|███▎ | 1/3 [00:07<00:15, 7.55s/it] |
|
|
Fetching 3 files: 67%|██████▋ | 2/3 [00:08<00:03, 3.72s/it] |
|
|
Fetching 3 files: 100%|██████████| 3/3 [00:08<00:00, 2.87s/it] |
|
|
You are using full precision kernels, we will dequantize the model to bf16. To use the quantized model with quantization kernels, please set use_kernels=False |
|
|
|
|
|
Loading checkpoint shards: 0%| | 0/3 [00:00<?, ?it/s] |
|
|
Loading checkpoint shards: 33%|███▎ | 1/3 [00:02<00:04, 2.34s/it] |
|
|
Loading checkpoint shards: 67%|██████▋ | 2/3 [00:04<00:02, 2.28s/it] |
|
|
Loading checkpoint shards: 100%|██████████| 3/3 [00:05<00:00, 1.82s/it] |
|
|
Loading checkpoint shards: 100%|██████████| 3/3 [00:05<00:00, 1.95s/it] |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
|
|
|
Fetching 6 files: 0%| | 0/6 [00:00<?, ?it/s] |
|
|
Fetching 6 files: 17%|█▋ | 1/6 [00:00<00:00, 5.35it/s] |
|
|
Fetching 6 files: 50%|█████ | 3/6 [00:00<00:00, 6.55it/s] |
|
|
Fetching 6 files: 100%|██████████| 6/6 [00:00<00:00, 12.81it/s] |
|
|
/tmp/uvnote-run-og9tszom/home/.cache/uv/environments-v2/setup-d9b6d9dd835772a9/lib/python3.13/site-packages/kernels/layer.py:868: UserWarning: |
|
|
No kernel mapping found for layer `None`. Check if the layer name matches one of the kernels in the mapping or add the kernel you want to use to the mapping. Defaulting to original forward implementation. |
|
|
warnings.warn( |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
/tmp/uvnote-run-og9tszom/home/.cache/uv/environments-v2/setup-d9b6d9dd835772a9/lib/python3.13/site-packages/kernels/layer.py:868: UserWarning: |
|
|
No kernel mapping found for layer `None`. Check if the layer name matches one of the kernels in the mapping or add the kernel you want to use to the mapping. Defaulting to original forward implementation. |
|
|
warnings.warn( |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe` |
|
|
INFO:root:Using layer `Yamoe` from repo `drbh/yamoe` (revision: v0.3.0) for layer `Yamoe`</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<h1>Reference kernel</h1> |
|
|
<div class="cell" id="cell-setup2"> |
|
|
<div class="cell-header"> |
|
|
<span class="collapse-indicators"> |
|
|
<span onclick="toggleCode('setup2')" style="cursor: pointer;">▼ code</span> |
|
|
<span onclick="toggleOutput('setup2')" style="cursor: pointer;">▼ output</span> |
|
|
<span id="uv-indicator-setup2" onclick="toggleUvLogsFromHeader('setup2')" style="cursor: pointer;">▶ uv-logs</span> |
|
|
</span> | |
|
|
Cell: setup2 | 140.15s |
|
|
| <button class="run-btn" onclick="runCell('setup2')">▶ run</button> |
|
|
<button class="copy-btn" onclick="copyCell('setup2')">Copy</button> |
|
|
<a href="cells/setup2.py" target="_blank" class="raw-btn">Raw</a> |
|
|
</div> |
|
|
<div id="code-setup2" class="cell-code" data-lines="115"> |
|
|
<div class="highlight-with-lines"> |
|
|
<div class="line-numbers" id="lines-setup2"> |
|
|
<a class="line-number" data-cell="setup2" data-line="1" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 1, true);">1</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="2" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 2, true);">2</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="3" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 3, true);">3</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="4" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 4, true);">4</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="5" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 5, true);">5</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="6" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 6, true);">6</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="7" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 7, true);">7</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="8" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 8, true);">8</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="9" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 9, true);">9</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="10" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 10, true);">10</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="11" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 11, true);">11</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="12" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 12, true);">12</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="13" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 13, true);">13</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="14" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 14, true);">14</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="15" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 15, true);">15</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="16" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 16, true);">16</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="17" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 17, true);">17</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="18" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 18, true);">18</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="19" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 19, true);">19</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="20" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 20, true);">20</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="21" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 21, true);">21</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="22" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 22, true);">22</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="23" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 23, true);">23</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="24" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 24, true);">24</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="25" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 25, true);">25</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="26" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 26, true);">26</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="27" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 27, true);">27</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="28" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 28, true);">28</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="29" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 29, true);">29</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="30" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 30, true);">30</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="31" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 31, true);">31</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="32" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 32, true);">32</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="33" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 33, true);">33</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="34" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 34, true);">34</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="35" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 35, true);">35</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="36" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 36, true);">36</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="37" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 37, true);">37</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="38" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 38, true);">38</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="39" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 39, true);">39</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="40" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 40, true);">40</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="41" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 41, true);">41</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="42" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 42, true);">42</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="43" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 43, true);">43</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="44" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 44, true);">44</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="45" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 45, true);">45</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="46" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 46, true);">46</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="47" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 47, true);">47</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="48" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 48, true);">48</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="49" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 49, true);">49</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="50" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 50, true);">50</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="51" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 51, true);">51</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="52" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 52, true);">52</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="53" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 53, true);">53</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="54" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 54, true);">54</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="55" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 55, true);">55</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="56" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 56, true);">56</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="57" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 57, true);">57</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="58" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 58, true);">58</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="59" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 59, true);">59</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="60" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 60, true);">60</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="61" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 61, true);">61</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="62" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 62, true);">62</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="63" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 63, true);">63</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="64" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 64, true);">64</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="65" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 65, true);">65</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="66" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 66, true);">66</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="67" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 67, true);">67</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="68" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 68, true);">68</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="69" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 69, true);">69</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="70" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 70, true);">70</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="71" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 71, true);">71</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="72" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 72, true);">72</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="73" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 73, true);">73</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="74" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 74, true);">74</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="75" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 75, true);">75</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="76" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 76, true);">76</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="77" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 77, true);">77</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="78" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 78, true);">78</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="79" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 79, true);">79</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="80" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 80, true);">80</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="81" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 81, true);">81</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="82" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 82, true);">82</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="83" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 83, true);">83</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="84" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 84, true);">84</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="85" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 85, true);">85</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="86" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 86, true);">86</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="87" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 87, true);">87</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="88" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 88, true);">88</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="89" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 89, true);">89</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="90" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 90, true);">90</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="91" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 91, true);">91</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="92" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 92, true);">92</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="93" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 93, true);">93</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="94" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 94, true);">94</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="95" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 95, true);">95</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="96" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 96, true);">96</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="97" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 97, true);">97</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="98" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 98, true);">98</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="99" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 99, true);">99</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="100" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 100, true);">100</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="101" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 101, true);">101</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="102" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 102, true);">102</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="103" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 103, true);">103</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="104" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 104, true);">104</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="105" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 105, true);">105</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="106" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 106, true);">106</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="107" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 107, true);">107</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="108" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 108, true);">108</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="109" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 109, true);">109</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="110" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 110, true);">110</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="111" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 111, true);">111</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="112" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 112, true);">112</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="113" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 113, true);">113</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="114" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 114, true);">114</a> |
|
|
<a class="line-number" data-cell="setup2" data-line="115" href="#cell-setup2" onclick="event.preventDefault(); selectCellLine('setup2', 115, true);">115</a> |
|
|
</div> |
|
|
<div class="code-wrap"> |
|
|
<div class="highlight"><pre><span></span><span class="c1"># /// script</span> |
|
|
<span class="c1"># requires-python = ">=3.12"</span> |
|
|
<span class="c1"># dependencies = [</span> |
|
|
<span class="c1"># "accelerate>=1.10.1",</span> |
|
|
<span class="c1"># "torch>=2.7.0",</span> |
|
|
<span class="c1"># "kernels==0.10.0",</span> |
|
|
<span class="c1"># "transformers@https://github.com/huggingface/transformers.git",</span> |
|
|
<span class="c1"># "ipdb>=0.13.13",</span> |
|
|
<span class="c1"># "matplotlib>=3.7.2",</span> |
|
|
<span class="c1"># "numpy>=1.24.3",</span> |
|
|
<span class="c1"># ]</span> |
|
|
<span class="c1"># ///</span> |
|
|
|
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">torch</span> |
|
|
<span class="kn">from</span><span class="w"> </span><span class="nn">transformers</span><span class="w"> </span><span class="kn">import</span> <span class="n">GptOssForCausalLM</span><span class="p">,</span> <span class="n">PreTrainedTokenizerFast</span><span class="p">,</span> <span class="n">Mxfp4Config</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">time</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">torch.nn</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">nn</span> |
|
|
<span class="kn">from</span><span class="w"> </span><span class="nn">kernels</span><span class="w"> </span><span class="kn">import</span> <span class="n">register_kernel_mapping</span><span class="p">,</span> <span class="n">Mode</span><span class="p">,</span> <span class="n">LayerRepository</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">sys</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">torch.profiler</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">gc</span> |
|
|
<span class="kn">import</span><span class="w"> </span><span class="nn">logging</span> |
|
|
|
|
|
<span class="c1"># set to debug logging</span> |
|
|
<span class="n">logging</span><span class="o">.</span><span class="n">basicConfig</span><span class="p">(</span><span class="n">level</span><span class="o">=</span><span class="n">logging</span><span class="o">.</span><span class="n">INFO</span><span class="p">)</span> |
|
|
|
|
|
<span class="k">def</span><span class="w"> </span><span class="nf">reset_peak_memory_stats</span><span class="p">():</span> |
|
|
<span class="w"> </span><span class="sd">"""Clear CUDA cache and reset memory allocation counters."""</span> |
|
|
<span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">empty_cache</span><span class="p">()</span> |
|
|
<span class="k">if</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">is_available</span><span class="p">():</span> |
|
|
<span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">reset_peak_memory_stats</span><span class="p">()</span> |
|
|
<span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span> |
|
|
|
|
|
<span class="k">def</span><span class="w"> </span><span class="nf">get_memory_stats</span><span class="p">():</span> |
|
|
<span class="w"> </span><span class="sd">"""Get current and peak CUDA memory usage."""</span> |
|
|
<span class="k">if</span> <span class="ow">not</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">is_available</span><span class="p">():</span> |
|
|
<span class="k">return</span> <span class="p">{</span><span class="s2">"allocated_gb"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">"peak_gb"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">"reserved_gb"</span><span class="p">:</span> <span class="mi">0</span><span class="p">}</span> |
|
|
<span class="k">return</span> <span class="p">{</span> |
|
|
<span class="s2">"allocated_gb"</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">memory_allocated</span><span class="p">()</span> <span class="o">/</span> <span class="mf">1e9</span><span class="p">,</span> |
|
|
<span class="s2">"peak_gb"</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">max_memory_allocated</span><span class="p">()</span> <span class="o">/</span> <span class="mf">1e9</span><span class="p">,</span> |
|
|
<span class="s2">"reserved_gb"</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">memory_reserved</span><span class="p">()</span> <span class="o">/</span> <span class="mf">1e9</span><span class="p">,</span> |
|
|
<span class="p">}</span> |
|
|
|
|
|
<span class="k">def</span><span class="w"> </span><span class="nf">override_kernel_layer_name</span><span class="p">(</span><span class="n">cls_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> <span class="o">-></span> <span class="nb">bool</span><span class="p">:</span> |
|
|
<span class="w"> </span><span class="sd">"""Helper to dynamically override the kernel_layer_name in a model class."""</span> |
|
|
<span class="k">for</span> <span class="n">mod</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">modules</span><span class="o">.</span><span class="n">values</span><span class="p">():</span> |
|
|
<span class="k">if</span> <span class="n">mod</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span> |
|
|
<span class="k">continue</span> |
|
|
<span class="n">obj</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">mod</span><span class="p">,</span> <span class="n">cls_name</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span> |
|
|
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="nb">type</span><span class="p">)</span> <span class="ow">and</span> <span class="nb">issubclass</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">nn</span><span class="o">.</span><span class="n">Module</span><span class="p">):</span> |
|
|
<span class="nb">setattr</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="s2">"kernel_layer_name"</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> |
|
|
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Overrode </span><span class="si">{</span><span class="n">cls_name</span><span class="si">}</span><span class="s2">.kernel_layer_name to </span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> |
|
|
<span class="k">return</span> <span class="kc">True</span> |
|
|
<span class="k">return</span> <span class="kc">False</span> |
|
|
|
|
|
|
|
|
<span class="c1"># Init the model the normal way</span> |
|
|
<span class="n">model_id</span> <span class="o">=</span> <span class="s2">"openai/gpt-oss-20b"</span> |
|
|
<span class="n">tokenizer</span> <span class="o">=</span> <span class="n">PreTrainedTokenizerFast</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_id</span><span class="p">)</span> |
|
|
<span class="n">quantization_config</span> <span class="o">=</span> <span class="n">Mxfp4Config</span><span class="p">(</span><span class="n">dequantize</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span> |
|
|
|
|
|
|
|
|
<span class="kn">from</span><span class="w"> </span><span class="nn">kernels</span><span class="w"> </span><span class="kn">import</span> <span class="n">replace_kernel_forward_from_hub</span><span class="p">,</span> <span class="n">register_kernel_mapping</span><span class="p">,</span> <span class="n">LayerRepository</span><span class="p">,</span> <span class="n">Mode</span> |
|
|
|
|
|
<span class="kn">from</span><span class="w"> </span><span class="nn">transformers.models.gpt_oss.modeling_gpt_oss</span><span class="w"> </span><span class="kn">import</span> <span class="n">GptOssMLP</span><span class="p">,</span> <span class="n">GptOssRMSNorm</span> |
|
|
|
|
|
<span class="n">replace_kernel_forward_from_hub</span><span class="p">(</span><span class="n">GptOssRMSNorm</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span> <span class="c1"># direct, type-safe</span> |
|
|
<span class="n">custom_mapping</span> <span class="o">=</span> <span class="p">{</span> |
|
|
<span class="s2">"Yamoe"</span><span class="p">:</span> <span class="p">{</span> |
|
|
<span class="s2">"cuda"</span><span class="p">:</span> <span class="p">{</span> |
|
|
<span class="n">Mode</span><span class="o">.</span><span class="n">INFERENCE</span><span class="p">:</span> <span class="n">LayerRepository</span><span class="p">(</span> |
|
|
<span class="n">repo_id</span><span class="o">=</span><span class="s2">"drbh/yamoe"</span><span class="p">,</span> |
|
|
<span class="n">layer_name</span><span class="o">=</span><span class="s2">"Yamoe"</span><span class="p">,</span> |
|
|
<span class="n">revision</span><span class="o">=</span><span class="s2">"v0.3.0"</span><span class="p">,</span> |
|
|
<span class="p">)</span> |
|
|
<span class="p">}</span> |
|
|
<span class="p">}</span> |
|
|
<span class="p">}</span> |
|
|
<span class="n">register_kernel_mapping</span><span class="p">(</span><span class="n">custom_mapping</span><span class="p">)</span> |
|
|
|
|
|
|
|
|
<span class="n">model</span> <span class="o">=</span> <span class="n">GptOssForCausalLM</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span> |
|
|
<span class="n">model_id</span><span class="p">,</span> |
|
|
<span class="n">dtype</span><span class="o">=</span><span class="s2">"bfloat16"</span><span class="p">,</span> |
|
|
<span class="n">device_map</span><span class="o">=</span><span class="s2">"auto"</span><span class="p">,</span> |
|
|
<span class="n">use_kernels</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> |
|
|
<span class="n">quantization_config</span><span class="o">=</span><span class="n">quantization_config</span><span class="p">,</span> |
|
|
<span class="p">)</span><span class="o">.</span><span class="n">eval</span><span class="p">()</span> |
|
|
|
|
|
<span class="n">messages</span> <span class="o">=</span> <span class="p">[</span> |
|
|
<span class="p">{</span><span class="s2">"role"</span><span class="p">:</span> <span class="s2">"system"</span><span class="p">,</span> <span class="s2">"content"</span><span class="p">:</span> <span class="s2">"What is Tensor Parallelism?"</span><span class="p">},</span> |
|
|
<span class="p">]</span> |
|
|
|
|
|
<span class="n">inputs</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="o">.</span><span class="n">apply_chat_template</span><span class="p">(</span> |
|
|
<span class="n">messages</span><span class="p">,</span> |
|
|
<span class="n">add_generation_prompt</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> |
|
|
<span class="n">return_tensors</span><span class="o">=</span><span class="s2">"pt"</span><span class="p">,</span> |
|
|
<span class="n">return_dict</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> |
|
|
<span class="n">reasoning_effort</span><span class="o">=</span><span class="s2">"low"</span><span class="p">,</span> |
|
|
<span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="s2">"cuda"</span><span class="p">)</span> |
|
|
|
|
|
<span class="n">max_tokens</span> <span class="o">=</span> <span class="mi">512</span> |
|
|
|
|
|
<span class="k">with</span> <span class="n">torch</span><span class="o">.</span><span class="n">inference_mode</span><span class="p">():</span> |
|
|
<span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span> |
|
|
<span class="n">generated</span> <span class="o">=</span> <span class="n">model</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span> |
|
|
<span class="o">**</span><span class="n">inputs</span><span class="p">,</span> |
|
|
<span class="n">max_new_tokens</span><span class="o">=</span><span class="n">max_tokens</span><span class="p">,</span> |
|
|
<span class="n">do_sample</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> |
|
|
<span class="n">temperature</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> |
|
|
<span class="p">)</span> |
|
|
<span class="n">end_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span> |
|
|
|
|
|
<span class="nb">print</span><span class="p">(</span><span class="n">tokenizer</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="n">generated</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">skip_special_tokens</span><span class="o">=</span><span class="kc">False</span><span class="p">))</span> |
|
|
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Generation took </span><span class="si">{</span><span class="n">end_time</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">start_time</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2"> seconds"</span><span class="p">)</span> |
|
|
</pre></div> |
|
|
|
|
|
<div class="code-line-highlight" id="line-highlight-setup2"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div id="output-setup2" class="cell-output"> |
|
|
<div class="cell-stdout"><|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI. |
|
|
Knowledge cutoff: 2024-06 |
|
|
Current date: 2025-09-23 |
|
|
|
|
|
Reasoning: low |
|
|
|
|
|
# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions |
|
|
|
|
|
What is Tensor Parallelism? |
|
|
|
|
|
<|end|><|start|>assistant<|channel|>analysis<|message|>We need to explain what Tensor Parallelism is. It's a concept in distributed training of large language models. It refers to splitting the weight matrices (tensors) across multiple devices. Provide details: how it works, benefits, challenges, typical frameworks, etc. Also mention difference from data parallelism, pipeline parallelism. Provide example: splitting a weight matrix across GPUs, each GPU holds a slice, compute partial results, then gather. Provide mention of communication overhead, scaling, etc. Also mention that it's used in large models like GPT-3, Megatron-LM, DeepSpeed, etc. Provide explanation of how it reduces memory usage, increases throughput. Provide mention of "tensor model parallelism" vs "tensor parallelism" synonyms. Provide mention of "tensor parallelism" in context of huggingface accelerate, DeepSpeed, Megatron. Provide mention of "tensor parallelism" in the context of "tensor parallelism" in the "DeepSpeed ZeRO-Offload" or "ZeRO-3" etc. Provide mention of "tensor parallelism" in the context of "tensor parallelism" in "DeepSpeed" and "Megatron-LM" and "DeepSpeed's ZeRO" and "DeepSpeed's ZeRO-3" and "DeepSpeed's ZeRO-2" etc. Provide mention of "tensor parallelism" in the context of "tensor parallelism" in "DeepSpeed's ZeRO-3" and "DeepSpeed's ZeRO-2" etc. Provide mention of "tensor parallelism" in the context of "tensor parallelism" in "DeepSpeed's ZeRO-3" and "DeepSpeed's ZeRO-2" etc. Provide mention of "tensor parallelism" in the context of "tensor parallelism" in "DeepSpeed's ZeRO-3" and "DeepSpeed's ZeRO-2" etc. Provide mention of "tensor parallelism" in the context of "tensor parallelism" in "DeepSpeed's ZeRO-3" and "DeepSpeed's ZeRO-2" etc. Provide mention of "tensor parallelism" in the context of "tensor parallelism" in "DeepSpeed's ZeRO-3" and "DeepSpeed's ZeRO-2" etc. Provide mention of "tensor parallelism" in the context of "tensor parallelism" in "DeepSpeed's ZeRO-3" and "DeepSpeed's ZeRO-2" etc. Provide mention of "tensor parallelism" in the |
|
|
Generation took 57.93 seconds |
|
|
</div> |
|
|
<div class="uv-install-logs" id="uv-logs-setup2"> |
|
|
<div class="uv-logs-header" onclick="toggleUvLogs(this)">▶ UV Install Logs</div> |
|
|
<div class="uv-logs-content" style="display: none;"> |
|
|
Downloading cpython-3.13.7-linux-x86_64-gnu (download) (32.0MiB) |
|
|
Downloading cpython-3.13.7-linux-x86_64-gnu (download) |
|
|
Updating https://github.com/huggingface/transformers.git (HEAD) |
|
|
Updated https://github.com/huggingface/transformers.git (99b0995138c17ef953959c70f35cb2bdc41111a2) |
|
|
Downloading numpy (15.9MiB) |
|
|
Downloading pygments (1.2MiB) |
|
|
Building transformers @ git+https://github.com/huggingface/transformers.git@99b0995138c17ef953959c70f35cb2bdc41111a2 |
|
|
Downloading pillow (6.3MiB) |
|
|
Downloading nvidia-cuda-nvrtc-cu12 (84.0MiB) |
|
|
Downloading nvidia-cusolver-cu12 (255.1MiB) |
|
|
Downloading networkx (1.9MiB) |
|
|
Downloading nvidia-cufile-cu12 (1.1MiB) |
|
|
Downloading tokenizers (3.1MiB) |
|
|
Downloading hf-xet (3.0MiB) |
|
|
Downloading nvidia-cublas-cu12 (566.8MiB) |
|
|
Downloading nvidia-cudnn-cu12 (674.0MiB) |
|
|
Downloading nvidia-cufft-cu12 (184.2MiB) |
|
|
Downloading sympy (6.0MiB) |
|
|
Downloading nvidia-curand-cu12 (60.7MiB) |
|
|
Downloading nvidia-cusparse-cu12 (274.9MiB) |
|
|
Downloading jedi (1.5MiB) |
|
|
Downloading nvidia-cusparselt-cu12 (273.9MiB) |
|
|
Downloading nvidia-nvjitlink-cu12 (37.4MiB) |
|
|
Downloading nvidia-nccl-cu12 (307.4MiB) |
|
|
Downloading nvidia-cuda-cupti-cu12 (9.8MiB) |
|
|
Downloading torch (846.8MiB) |
|
|
Downloading triton (148.4MiB) |
|
|
Downloading matplotlib (8.3MiB) |
|
|
Downloading kiwisolver (1.4MiB) |
|
|
Downloading fonttools (4.7MiB) |
|
|
Downloading nvidia-cufile-cu12 |
|
|
Downloading kiwisolver |
|
|
Downloading pygments |
|
|
Downloading hf-xet |
|
|
Downloading tokenizers |
|
|
Downloading networkx |
|
|
Downloading fonttools |
|
|
Downloading pillow |
|
|
Downloading matplotlib |
|
|
Downloading nvidia-cuda-cupti-cu12 |
|
|
Downloading numpy |
|
|
Downloading sympy |
|
|
Downloading nvidia-nvjitlink-cu12 |
|
|
Built transformers @ git+https://github.com/huggingface/transformers.git@99b0995138c17ef953959c70f35cb2bdc41111a2 |
|
|
Downloading jedi |
|
|
Downloading nvidia-curand-cu12 |
|
|
Downloading nvidia-cuda-nvrtc-cu12 |
|
|
Downloading triton |
|
|
Downloading nvidia-cufft-cu12 |
|
|
Downloading nvidia-cusolver-cu12 |
|
|
Downloading nvidia-cusparselt-cu12 |
|
|
Downloading nvidia-cusparse-cu12 |
|
|
Downloading nvidia-nccl-cu12 |
|
|
Downloading nvidia-cublas-cu12 |
|
|
Downloading nvidia-cudnn-cu12 |
|
|
Downloading torch |
|
|
Installed 69 packages in 460ms |
|
|
</div> |
|
|
</div> |
|
|
<div class="cell-stderr">Fetching 3 files: 0%| | 0/3 [00:00<?, ?it/s] |
|
|
Fetching 3 files: 33%|███▎ | 1/3 [00:07<00:14, 7.31s/it] |
|
|
Fetching 3 files: 67%|██████▋ | 2/3 [00:08<00:03, 3.67s/it] |
|
|
Fetching 3 files: 100%|██████████| 3/3 [00:08<00:00, 2.81s/it] |
|
|
You are using full precision kernels, we will dequantize the model to bf16. To use the quantized model with quantization kernels, please set use_kernels=False |
|
|
|
|
|
Loading checkpoint shards: 0%| | 0/3 [00:00<?, ?it/s] |
|
|
Loading checkpoint shards: 33%|███▎ | 1/3 [00:02<00:04, 2.35s/it] |
|
|
Loading checkpoint shards: 67%|██████▋ | 2/3 [00:04<00:02, 2.25s/it] |
|
|
Loading checkpoint shards: 100%|██████████| 3/3 [00:05<00:00, 1.80s/it] |
|
|
Loading checkpoint shards: 100%|██████████| 3/3 [00:05<00:00, 1.93s/it] |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
|
|
|
Fetching 66 files: 0%| | 0/66 [00:00<?, ?it/s] |
|
|
Fetching 66 files: 2%|▏ | 1/66 [00:00<00:17, 3.80it/s] |
|
|
Fetching 66 files: 9%|▉ | 6/66 [00:00<00:03, 19.77it/s] |
|
|
Fetching 66 files: 14%|█▎ | 9/66 [00:00<00:02, 19.05it/s] |
|
|
Fetching 66 files: 26%|██▌ | 17/66 [00:01<00:04, 10.13it/s] |
|
|
Fetching 66 files: 86%|████████▋ | 57/66 [00:01<00:00, 47.70it/s] |
|
|
Fetching 66 files: 100%|██████████| 66/66 [00:01<00:00, 36.16it/s] |
|
|
/tmp/uvnote-run-d2g9g4zl/home/.cache/uv/environments-v2/setup2-ea0d7cee95bc10c1/lib/python3.13/site-packages/kernels/layer.py:868: UserWarning: |
|
|
No kernel mapping found for layer `None`. Check if the layer name matches one of the kernels in the mapping or add the kernel you want to use to the mapping. Defaulting to original forward implementation. |
|
|
warnings.warn( |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
/tmp/uvnote-run-d2g9g4zl/home/.cache/uv/environments-v2/setup2-ea0d7cee95bc10c1/lib/python3.13/site-packages/kernels/layer.py:868: UserWarning: |
|
|
No kernel mapping found for layer `None`. Check if the layer name matches one of the kernels in the mapping or add the kernel you want to use to the mapping. Defaulting to original forward implementation. |
|
|
warnings.warn( |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP` |
|
|
INFO:root:Using layer `MegaBlocksMoeMLP` from repo `kernels-community/megablocks` (revision: main) for layer `MegaBlocksMoeMLP`</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
</body> |
|
|
</html> |