|
|
<!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'; |
|
|
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); |
|
|
})(); |
|
|
</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); |
|
|
} |
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.controls { |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
display: flex; |
|
|
gap: 0.5rem; |
|
|
z-index: 1000; |
|
|
} |
|
|
|
|
|
.menu-button { |
|
|
position: relative; |
|
|
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; |
|
|
} |
|
|
|
|
|
.menu-button:hover { |
|
|
color: var(--text-primary); |
|
|
background: var(--bg-tertiary); |
|
|
} |
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
.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; |
|
|
padding: 0.4rem 0.6rem; |
|
|
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; |
|
|
} |
|
|
|
|
|
.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); |
|
|
} |
|
|
.cell-header { |
|
|
background: var(--bg-secondary); |
|
|
padding: 0.5rem 1rem; |
|
|
border-bottom: 1px solid var(--border-primary); |
|
|
font-family: inherit; |
|
|
font-size: 0.85rem; |
|
|
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-primary); |
|
|
} |
|
|
.cell-output.collapsed { |
|
|
display: none; |
|
|
} |
|
|
.cell-stdout { |
|
|
background: var(--bg-tertiary); |
|
|
padding: 0.75rem; |
|
|
border-radius: 1px; |
|
|
margin: 0.25rem 0; |
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
.highlight-with-lines { |
|
|
display: flex; |
|
|
} |
|
|
.line-numbers { |
|
|
background: var(--bg-tertiary); |
|
|
padding: 0.75rem 0.5rem; |
|
|
font-family: 'Cascadia Mono', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, 'Consolas', monospace; |
|
|
font-size: 0.9rem; |
|
|
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: 1.5; |
|
|
} |
|
|
.highlight-with-lines .highlight { |
|
|
flex: 1; |
|
|
} |
|
|
.highlight-with-lines .highlight pre { |
|
|
padding-left: 0.75rem; |
|
|
} |
|
|
|
|
|
|
|
|
.cell-code.collapsed { |
|
|
display: none; |
|
|
} |
|
|
.cell-code.expanded { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.cell-code { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
|
|
|
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 } |
|
|
|
|
|
|
|
|
#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); |
|
|
border-left: 3px solid var(--text-link); |
|
|
} |
|
|
</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); |
|
|
} |
|
|
|
|
|
function toggleCode(cellId) { |
|
|
const codeElement = document.getElementById('code-' + cellId); |
|
|
if (codeElement) { |
|
|
codeElement.classList.toggle('collapsed'); |
|
|
updateIndicators(cellId); |
|
|
} |
|
|
} |
|
|
|
|
|
function toggleOutput(cellId) { |
|
|
const outputElement = document.getElementById('output-' + cellId); |
|
|
if (outputElement) { |
|
|
outputElement.classList.toggle('collapsed'); |
|
|
updateIndicators(cellId); |
|
|
} |
|
|
} |
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function updateThemeIcon() { |
|
|
const theme = document.documentElement.getAttribute('data-theme'); |
|
|
const toggle = document.querySelector('.theme-toggle'); |
|
|
if (toggle) { |
|
|
toggle.textContent = theme === 'dark' ? 'light' : 'dark'; |
|
|
} |
|
|
} |
|
|
|
|
|
function resetLayout() { |
|
|
try { |
|
|
|
|
|
const allKeys = Object.keys(localStorage); |
|
|
const uvnoteKeys = allKeys.filter(key => key.startsWith('uvnote-')); |
|
|
uvnoteKeys.forEach(k => localStorage.removeItem(k)); |
|
|
} 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; |
|
|
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 (_) {} |
|
|
} |
|
|
} |
|
|
|
|
|
function initializeWidgetVisibility() { |
|
|
const widgets = [ |
|
|
{ name: 'tools', selector: '.tools-widget' }, |
|
|
{ name: 'file-explorer', selector: '.file-explorer' }, |
|
|
{ name: 'minimap', selector: '.minimap' } |
|
|
]; |
|
|
|
|
|
widgets.forEach(({ name, selector }) => { |
|
|
const savedState = localStorage.getItem(`uvnote-widget-${name}`) || 'hidden'; |
|
|
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 widgets = [minimap, fileExplorer, tools].filter(el => el && getComputedStyle(el).display !== 'none'); |
|
|
if (!widgets.length) return; |
|
|
|
|
|
const order = [minimap, fileExplorer, tools].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; |
|
|
} |
|
|
|
|
|
|
|
|
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')); |
|
|
return; |
|
|
} |
|
|
document.body.dataset.tool = tool; |
|
|
localStorage.setItem('uvnote-active-tool', tool); |
|
|
setOverlayActive(true); |
|
|
_cursorVisible = true; |
|
|
} |
|
|
|
|
|
|
|
|
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 4; |
|
|
} |
|
|
|
|
|
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(); |
|
|
}; |
|
|
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(); |
|
|
}; |
|
|
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(); |
|
|
}; |
|
|
|
|
|
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'; |
|
|
}; |
|
|
|
|
|
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; |
|
|
|
|
|
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)); } catch (_) {} |
|
|
} |
|
|
|
|
|
function updateShapesFade() { |
|
|
const now = Date.now(); |
|
|
const fadeoutSeconds = getFadeoutTime(); |
|
|
|
|
|
|
|
|
if (fadeoutSeconds === 0) return; |
|
|
|
|
|
const fadeStartTime = Math.max(0, (fadeoutSeconds - 2) * 1000); |
|
|
const fadeEndTime = fadeoutSeconds * 1000; |
|
|
let needsUpdate = false; |
|
|
|
|
|
for (let i = _shapes.length - 1; i >= 0; i--) { |
|
|
const shape = _shapes[i]; |
|
|
if (!shape.createdAt) continue; |
|
|
|
|
|
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(); |
|
|
} |
|
|
} |
|
|
|
|
|
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(), |
|
|
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(), |
|
|
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(), |
|
|
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() { |
|
|
updateThemeIcon(); |
|
|
initMinimap(); |
|
|
initFileExplorer(); |
|
|
initTools(); |
|
|
initOverlay(); |
|
|
initializeWidgetVisibility(); |
|
|
layoutWidgetsStackedBottomRight(); |
|
|
window.addEventListener('resize', layoutWidgetsStackedBottomRight); |
|
|
|
|
|
|
|
|
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'); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
</script> |
|
|
</head> |
|
|
<body> |
|
|
<div class="controls"> |
|
|
<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="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> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="system-info"> |
|
|
<div class="system-info-header">Generated on:</div> |
|
|
<div class="system-info-content"> |
|
|
Linux x86_64 | Linux-5.15.0-1084-aws-x86_64-with-glibc2.31 |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="main-content"> |
|
|
<div class="cell"> |
|
|
<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 | 191.24s |
|
|
| <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"> |
|
|
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span class="normal"> 1</span> |
|
|
<span class="normal"> 2</span> |
|
|
<span class="normal"> 3</span> |
|
|
<span class="normal"> 4</span> |
|
|
<span class="normal"> 5</span> |
|
|
<span class="normal"> 6</span> |
|
|
<span class="normal"> 7</span> |
|
|
<span class="normal"> 8</span> |
|
|
<span class="normal"> 9</span> |
|
|
<span class="normal"> 10</span> |
|
|
<span class="normal"> 11</span> |
|
|
<span class="normal"> 12</span> |
|
|
<span class="normal"> 13</span> |
|
|
<span class="normal"> 14</span> |
|
|
<span class="normal"> 15</span> |
|
|
<span class="normal"> 16</span> |
|
|
<span class="normal"> 17</span> |
|
|
<span class="normal"> 18</span> |
|
|
<span class="normal"> 19</span> |
|
|
<span class="normal"> 20</span> |
|
|
<span class="normal"> 21</span> |
|
|
<span class="normal"> 22</span> |
|
|
<span class="normal"> 23</span> |
|
|
<span class="normal"> 24</span> |
|
|
<span class="normal"> 25</span> |
|
|
<span class="normal"> 26</span> |
|
|
<span class="normal"> 27</span> |
|
|
<span class="normal"> 28</span> |
|
|
<span class="normal"> 29</span> |
|
|
<span class="normal"> 30</span> |
|
|
<span class="normal"> 31</span> |
|
|
<span class="normal"> 32</span> |
|
|
<span class="normal"> 33</span> |
|
|
<span class="normal"> 34</span> |
|
|
<span class="normal"> 35</span> |
|
|
<span class="normal"> 36</span> |
|
|
<span class="normal"> 37</span> |
|
|
<span class="normal"> 38</span> |
|
|
<span class="normal"> 39</span> |
|
|
<span class="normal"> 40</span> |
|
|
<span class="normal"> 41</span> |
|
|
<span class="normal"> 42</span> |
|
|
<span class="normal"> 43</span> |
|
|
<span class="normal"> 44</span> |
|
|
<span class="normal"> 45</span> |
|
|
<span class="normal"> 46</span> |
|
|
<span class="normal"> 47</span> |
|
|
<span class="normal"> 48</span> |
|
|
<span class="normal"> 49</span> |
|
|
<span class="normal"> 50</span> |
|
|
<span class="normal"> 51</span> |
|
|
<span class="normal"> 52</span> |
|
|
<span class="normal"> 53</span> |
|
|
<span class="normal"> 54</span> |
|
|
<span class="normal"> 55</span> |
|
|
<span class="normal"> 56</span> |
|
|
<span class="normal"> 57</span> |
|
|
<span class="normal"> 58</span> |
|
|
<span class="normal"> 59</span> |
|
|
<span class="normal"> 60</span> |
|
|
<span class="normal"> 61</span> |
|
|
<span class="normal"> 62</span> |
|
|
<span class="normal"> 63</span> |
|
|
<span class="normal"> 64</span> |
|
|
<span class="normal"> 65</span> |
|
|
<span class="normal"> 66</span> |
|
|
<span class="normal"> 67</span> |
|
|
<span class="normal"> 68</span> |
|
|
<span class="normal"> 69</span> |
|
|
<span class="normal"> 70</span> |
|
|
<span class="normal"> 71</span> |
|
|
<span class="normal"> 72</span> |
|
|
<span class="normal"> 73</span> |
|
|
<span class="normal"> 74</span> |
|
|
<span class="normal"> 75</span> |
|
|
<span class="normal"> 76</span> |
|
|
<span class="normal"> 77</span> |
|
|
<span class="normal"> 78</span> |
|
|
<span class="normal"> 79</span> |
|
|
<span class="normal"> 80</span> |
|
|
<span class="normal"> 81</span> |
|
|
<span class="normal"> 82</span> |
|
|
<span class="normal"> 83</span> |
|
|
<span class="normal"> 84</span> |
|
|
<span class="normal"> 85</span> |
|
|
<span class="normal"> 86</span> |
|
|
<span class="normal"> 87</span> |
|
|
<span class="normal"> 88</span> |
|
|
<span class="normal"> 89</span> |
|
|
<span class="normal"> 90</span> |
|
|
<span class="normal"> 91</span> |
|
|
<span class="normal"> 92</span> |
|
|
<span class="normal"> 93</span> |
|
|
<span class="normal"> 94</span> |
|
|
<span class="normal"> 95</span> |
|
|
<span class="normal"> 96</span> |
|
|
<span class="normal"> 97</span> |
|
|
<span class="normal"> 98</span> |
|
|
<span class="normal"> 99</span> |
|
|
<span class="normal">100</span> |
|
|
<span class="normal">101</span> |
|
|
<span class="normal">102</span> |
|
|
<span class="normal">103</span> |
|
|
<span class="normal">104</span> |
|
|
<span class="normal">105</span> |
|
|
<span class="normal">106</span> |
|
|
<span class="normal">107</span> |
|
|
<span class="normal">108</span> |
|
|
<span class="normal">109</span> |
|
|
<span class="normal">110</span> |
|
|
<span class="normal">111</span> |
|
|
<span class="normal">112</span> |
|
|
<span class="normal">113</span> |
|
|
<span class="normal">114</span> |
|
|
<span class="normal">115</span> |
|
|
<span class="normal">116</span></pre></div></td><td class="code"><div><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></td></tr></table></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-22 |
|
|
|
|
|
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. Provide benefits: memory savings, compute scaling. Provide challenges: communication overhead, load balancing. Provide typical frameworks: Megatron-LM, DeepSpeed, etc. Provide mention of "tensor model parallelism" vs "tensor parallelism" synonyms. Provide explanation of how it's implemented: using torch.distributed, NCCL, etc. Provide mention of "tensor parallelism" in context of "DeepSpeed ZeRO Stage 3" or "Megatron-LM" etc. Provide mention of "tensor parallelism" as part of "model parallelism" in large language models. Provide mention of "tensor parallelism" vs "data parallelism" vs "pipeline parallelism". Provide mention of "tensor parallelism" as "splitting weight matrices across GPUs" and "communication of partial results". |
|
|
|
|
|
Also mention "tensor parallelism" can be used for linear layers, self-attention, feed-forward networks. Provide example: In GPT, each layer has a linear projection for Q, K, V, and output. These can be split across GPUs. Provide mention of "tensor parallelism" as "model parallelism" that splits the model across GPUs. Provide mention of "tensor parallelism" as "splitting the weight matrices across GPUs" and "communication of partial results". |
|
|
|
|
|
Also mention "tensor parallelism" can be used for "embedding tables" as well. Provide mention of "tensor parallelism" as "embedding table partitioning". |
|
|
|
|
|
Also mention "tensor parallelism" can be used for "attention heads" splitting across GPUs. |
|
|
|
|
|
Also mention "tensor parallelism" can be used for "feed-forward networks" splitting across GPUs. |
|
|
|
|
|
Also mention "tensor parallelism" can be used for "softmax" and "layernorm" across GPUs. |
|
|
|
|
|
Also mention "tensor parallelism" can be used for |
|
|
Generation took 97.20 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;"> |
|
|
Updating https://github.com/huggingface/transformers.git (HEAD) |
|
|
Updated https://github.com/huggingface/transformers.git (cbb290ec23ccd9b5c1d1ff4d333477449891debb) |
|
|
Downloading pillow (6.3MiB) |
|
|
Downloading numpy (15.9MiB) |
|
|
Building transformers @ git+https://github.com/huggingface/transformers.git@cbb290ec23ccd9b5c1d1ff4d333477449891debb |
|
|
Downloading nvidia-cuda-cupti-cu12 (9.8MiB) |
|
|
Downloading kiwisolver (1.4MiB) |
|
|
Downloading nvidia-nccl-cu12 (307.4MiB) |
|
|
Downloading tokenizers (3.1MiB) |
|
|
Downloading nvidia-cusparselt-cu12 (273.9MiB) |
|
|
Downloading nvidia-curand-cu12 (60.7MiB) |
|
|
Downloading nvidia-cuda-nvrtc-cu12 (84.0MiB) |
|
|
Downloading pygments (1.2MiB) |
|
|
Downloading nvidia-cusolver-cu12 (255.1MiB) |
|
|
Downloading sympy (6.0MiB) |
|
|
Downloading jedi (1.5MiB) |
|
|
Downloading networkx (1.9MiB) |
|
|
Downloading nvidia-cudnn-cu12 (674.0MiB) |
|
|
Downloading nvidia-cublas-cu12 (566.8MiB) |
|
|
Downloading nvidia-cusparse-cu12 (274.9MiB) |
|
|
Downloading nvidia-nvjitlink-cu12 (37.4MiB) |
|
|
Downloading nvidia-cufft-cu12 (184.2MiB) |
|
|
Downloading matplotlib (8.3MiB) |
|
|
Downloading nvidia-cufile-cu12 (1.1MiB) |
|
|
Downloading fonttools (4.7MiB) |
|
|
Downloading triton (148.4MiB) |
|
|
Downloading hf-xet (3.0MiB) |
|
|
Downloading torch (846.8MiB) |
|
|
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@cbb290ec23ccd9b5c1d1ff4d333477449891debb |
|
|
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 208ms |
|
|
</div> |
|
|
</div> |
|
|
<div class="cell-stderr">Fetching 3 files: 0%| | 0/3 [00:00<?, ?it/s] |
|
|
Fetching 3 files: 33%|███▎ | 1/3 [00:13<00:26, 13.18s/it] |
|
|
Fetching 3 files: 67%|██████▋ | 2/3 [00:17<00:08, 8.21s/it] |
|
|
Fetching 3 files: 100%|██████████| 3/3 [00:17<00:00, 5.97s/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:03<00:06, 3.22s/it] |
|
|
Loading checkpoint shards: 67%|██████▋ | 2/3 [00:06<00:03, 3.13s/it] |
|
|
Loading checkpoint shards: 100%|██████████| 3/3 [00:08<00:00, 2.49s/it] |
|
|
Loading checkpoint shards: 100%|██████████| 3/3 [00:08<00:00, 2.67s/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.14it/s] |
|
|
Fetching 6 files: 50%|█████ | 3/6 [00:00<00:00, 6.83it/s] |
|
|
Fetching 6 files: 100%|██████████| 6/6 [00:00<00:00, 13.22it/s] |
|
|
/tmp/uvnote-run-he2n6v96/home/.cache/uv/environments-v2/setup-30bb029f3f83f37d/lib/python3.12/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-he2n6v96/home/.cache/uv/environments-v2/setup-30bb029f3f83f37d/lib/python3.12/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"> |
|
|
<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 | 197.24s |
|
|
| <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"> |
|
|
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span class="normal"> 1</span> |
|
|
<span class="normal"> 2</span> |
|
|
<span class="normal"> 3</span> |
|
|
<span class="normal"> 4</span> |
|
|
<span class="normal"> 5</span> |
|
|
<span class="normal"> 6</span> |
|
|
<span class="normal"> 7</span> |
|
|
<span class="normal"> 8</span> |
|
|
<span class="normal"> 9</span> |
|
|
<span class="normal"> 10</span> |
|
|
<span class="normal"> 11</span> |
|
|
<span class="normal"> 12</span> |
|
|
<span class="normal"> 13</span> |
|
|
<span class="normal"> 14</span> |
|
|
<span class="normal"> 15</span> |
|
|
<span class="normal"> 16</span> |
|
|
<span class="normal"> 17</span> |
|
|
<span class="normal"> 18</span> |
|
|
<span class="normal"> 19</span> |
|
|
<span class="normal"> 20</span> |
|
|
<span class="normal"> 21</span> |
|
|
<span class="normal"> 22</span> |
|
|
<span class="normal"> 23</span> |
|
|
<span class="normal"> 24</span> |
|
|
<span class="normal"> 25</span> |
|
|
<span class="normal"> 26</span> |
|
|
<span class="normal"> 27</span> |
|
|
<span class="normal"> 28</span> |
|
|
<span class="normal"> 29</span> |
|
|
<span class="normal"> 30</span> |
|
|
<span class="normal"> 31</span> |
|
|
<span class="normal"> 32</span> |
|
|
<span class="normal"> 33</span> |
|
|
<span class="normal"> 34</span> |
|
|
<span class="normal"> 35</span> |
|
|
<span class="normal"> 36</span> |
|
|
<span class="normal"> 37</span> |
|
|
<span class="normal"> 38</span> |
|
|
<span class="normal"> 39</span> |
|
|
<span class="normal"> 40</span> |
|
|
<span class="normal"> 41</span> |
|
|
<span class="normal"> 42</span> |
|
|
<span class="normal"> 43</span> |
|
|
<span class="normal"> 44</span> |
|
|
<span class="normal"> 45</span> |
|
|
<span class="normal"> 46</span> |
|
|
<span class="normal"> 47</span> |
|
|
<span class="normal"> 48</span> |
|
|
<span class="normal"> 49</span> |
|
|
<span class="normal"> 50</span> |
|
|
<span class="normal"> 51</span> |
|
|
<span class="normal"> 52</span> |
|
|
<span class="normal"> 53</span> |
|
|
<span class="normal"> 54</span> |
|
|
<span class="normal"> 55</span> |
|
|
<span class="normal"> 56</span> |
|
|
<span class="normal"> 57</span> |
|
|
<span class="normal"> 58</span> |
|
|
<span class="normal"> 59</span> |
|
|
<span class="normal"> 60</span> |
|
|
<span class="normal"> 61</span> |
|
|
<span class="normal"> 62</span> |
|
|
<span class="normal"> 63</span> |
|
|
<span class="normal"> 64</span> |
|
|
<span class="normal"> 65</span> |
|
|
<span class="normal"> 66</span> |
|
|
<span class="normal"> 67</span> |
|
|
<span class="normal"> 68</span> |
|
|
<span class="normal"> 69</span> |
|
|
<span class="normal"> 70</span> |
|
|
<span class="normal"> 71</span> |
|
|
<span class="normal"> 72</span> |
|
|
<span class="normal"> 73</span> |
|
|
<span class="normal"> 74</span> |
|
|
<span class="normal"> 75</span> |
|
|
<span class="normal"> 76</span> |
|
|
<span class="normal"> 77</span> |
|
|
<span class="normal"> 78</span> |
|
|
<span class="normal"> 79</span> |
|
|
<span class="normal"> 80</span> |
|
|
<span class="normal"> 81</span> |
|
|
<span class="normal"> 82</span> |
|
|
<span class="normal"> 83</span> |
|
|
<span class="normal"> 84</span> |
|
|
<span class="normal"> 85</span> |
|
|
<span class="normal"> 86</span> |
|
|
<span class="normal"> 87</span> |
|
|
<span class="normal"> 88</span> |
|
|
<span class="normal"> 89</span> |
|
|
<span class="normal"> 90</span> |
|
|
<span class="normal"> 91</span> |
|
|
<span class="normal"> 92</span> |
|
|
<span class="normal"> 93</span> |
|
|
<span class="normal"> 94</span> |
|
|
<span class="normal"> 95</span> |
|
|
<span class="normal"> 96</span> |
|
|
<span class="normal"> 97</span> |
|
|
<span class="normal"> 98</span> |
|
|
<span class="normal"> 99</span> |
|
|
<span class="normal">100</span> |
|
|
<span class="normal">101</span> |
|
|
<span class="normal">102</span> |
|
|
<span class="normal">103</span> |
|
|
<span class="normal">104</span> |
|
|
<span class="normal">105</span> |
|
|
<span class="normal">106</span> |
|
|
<span class="normal">107</span> |
|
|
<span class="normal">108</span> |
|
|
<span class="normal">109</span> |
|
|
<span class="normal">110</span> |
|
|
<span class="normal">111</span> |
|
|
<span class="normal">112</span> |
|
|
<span class="normal">113</span> |
|
|
<span class="normal">114</span> |
|
|
<span class="normal">115</span></pre></div></td><td class="code"><div><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></td></tr></table></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-22 |
|
|
|
|
|
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 answer: "What is Tensor Parallelism?" Provide explanation.<|end|><|start|>assistant<|channel|>final<|message|>**Tensor Parallelism** is a technique used in large‑scale deep‑learning training to split the computation of a single tensor (e.g., a weight matrix or an activation) across multiple GPUs or other accelerators. Instead of each device holding a full copy of the tensor, the tensor is partitioned—usually along one dimension (rows or columns)—and each device processes only its slice. The partial results are then combined (e.g., summed) to produce the final output. |
|
|
|
|
|
Key points: |
|
|
|
|
|
| Feature | Description | |
|
|
|---------|-------------| |
|
|
| **Purpose** | Reduce memory footprint per device and increase throughput for very large models. | |
|
|
| **How it works** | A weight matrix \(W\) of shape \((M, N)\) is split into \(k\) parts along one axis: \(W = [W_1, W_2, …, W_k]\). Each GPU holds one part and performs its local matrix multiplication with the input. The partial outputs are then summed (or concatenated) to form the final result. | |
|
|
| **Typical use‑case** | Training transformer models with billions of parameters where a single weight matrix would not fit on one GPU. | |
|
|
| **Relation to other parallelism** | It is one of several parallelism strategies: *data parallelism* (replicating the whole model across devices), *model parallelism* (splitting the model into sub‑modules), and *pipeline parallelism* (splitting the model into stages). Tensor parallelism is a fine‑grained form of model parallelism. | |
|
|
| **Implementation** | Frameworks like Megatron‑LM, DeepSpeed, and PyTorch’s `torch.distributed` provide primitives for tensor‑parallel operations. | |
|
|
| **Benefits** | • Lower per‑device memory usage. <br>• Enables training of models that would otherwise be impossible on a single device. <br>• Can improve compute utilization when combined with other parallelism strategies. | |
|
|
| **Challenges** | • Requires careful communication (e.g., all‑reduce) to combine partial results. <br>• Load balancing and communication overhead can become bottlenecks. <br>• Implementation complexity increases. | |
|
|
|
|
|
In short, **tensor parallelism** distributes the *internal* tensors of a model across multiple devices, allowing each device to work on a smaller piece of the tensor and then combine the results, |
|
|
Generation took 103.82 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;"> |
|
|
Updating https://github.com/huggingface/transformers.git (HEAD) |
|
|
Updated https://github.com/huggingface/transformers.git (cbb290ec23ccd9b5c1d1ff4d333477449891debb) |
|
|
Downloading nvidia-cuda-cupti-cu12 (9.8MiB) |
|
|
Downloading pillow (6.3MiB) |
|
|
Downloading fonttools (4.7MiB) |
|
|
Downloading jedi (1.5MiB) |
|
|
Downloading kiwisolver (1.4MiB) |
|
|
Building transformers @ git+https://github.com/huggingface/transformers.git@cbb290ec23ccd9b5c1d1ff4d333477449891debb |
|
|
Downloading nvidia-cuda-nvrtc-cu12 (84.0MiB) |
|
|
Downloading numpy (15.9MiB) |
|
|
Downloading matplotlib (8.3MiB) |
|
|
Downloading tokenizers (3.1MiB) |
|
|
Downloading nvidia-cufft-cu12 (184.2MiB) |
|
|
Downloading networkx (1.9MiB) |
|
|
Downloading nvidia-cusparse-cu12 (274.9MiB) |
|
|
Downloading nvidia-nvjitlink-cu12 (37.4MiB) |
|
|
Downloading nvidia-cufile-cu12 (1.1MiB) |
|
|
Downloading hf-xet (3.0MiB) |
|
|
Downloading nvidia-cudnn-cu12 (674.0MiB) |
|
|
Downloading nvidia-nccl-cu12 (307.4MiB) |
|
|
Downloading pygments (1.2MiB) |
|
|
Downloading nvidia-cublas-cu12 (566.8MiB) |
|
|
Downloading triton (148.4MiB) |
|
|
Downloading sympy (6.0MiB) |
|
|
Downloading nvidia-cusparselt-cu12 (273.9MiB) |
|
|
Downloading nvidia-curand-cu12 (60.7MiB) |
|
|
Downloading nvidia-cusolver-cu12 (255.1MiB) |
|
|
Downloading torch (846.8MiB) |
|
|
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 |
|
|
Downloading jedi |
|
|
Built transformers @ git+https://github.com/huggingface/transformers.git@cbb290ec23ccd9b5c1d1ff4d333477449891debb |
|
|
Downloading nvidia-curand-cu12 |
|
|
Downloading nvidia-cuda-nvrtc-cu12 |
|
|
Downloading triton |
|
|
Downloading nvidia-cufft-cu12 |
|
|
Downloading nvidia-cusolver-cu12 |
|
|
Downloading nvidia-cusparse-cu12 |
|
|
Downloading nvidia-cusparselt-cu12 |
|
|
Downloading nvidia-nccl-cu12 |
|
|
Downloading nvidia-cublas-cu12 |
|
|
Downloading nvidia-cudnn-cu12 |
|
|
Downloading torch |
|
|
Installed 69 packages in 208ms |
|
|
</div> |
|
|
</div> |
|
|
<div class="cell-stderr">Fetching 3 files: 0%| | 0/3 [00:00<?, ?it/s] |
|
|
Fetching 3 files: 33%|███▎ | 1/3 [00:13<00:27, 13.63s/it] |
|
|
Fetching 3 files: 67%|██████▋ | 2/3 [00:17<00:07, 7.65s/it] |
|
|
Fetching 3 files: 100%|██████████| 3/3 [00:17<00:00, 5.70s/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:03<00:06, 3.23s/it] |
|
|
Loading checkpoint shards: 67%|██████▋ | 2/3 [00:06<00:03, 3.14s/it] |
|
|
Loading checkpoint shards: 100%|██████████| 3/3 [00:08<00:00, 2.49s/it] |
|
|
Loading checkpoint shards: 100%|██████████| 3/3 [00:08<00:00, 2.68s/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:20, 3.25it/s] |
|
|
Fetching 66 files: 17%|█▋ | 11/66 [00:00<00:01, 28.26it/s] |
|
|
Fetching 66 files: 26%|██▌ | 17/66 [00:00<00:02, 18.20it/s] |
|
|
Fetching 66 files: 100%|██████████| 66/66 [00:00<00:00, 69.67it/s] |
|
|
/tmp/uvnote-run-0ew4aumc/home/.cache/uv/environments-v2/setup2-d4e9795d8c8c2492/lib/python3.12/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-0ew4aumc/home/.cache/uv/environments-v2/setup2-d4e9795d8c8c2492/lib/python3.12/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> |