Stacks a Colab-like JupyterLab and Studio experience on top of the
existing Blackwell image. Additive only: the training stack, CUDA/torch
pinning, and the Studio/JupyterLab/sshd service trio are unchanged.
JupyterLab labextension (prebuilt in a throwaway builder stage, so the
runtime image stays Node-free):
- Unsloth Dark (Monokai) theme, adaptive light/dark by system preference
- Colab-style ArrowDown/Up cell navigation
- top-bar Unsloth logo (stock Jupyter logo disabled and locked)
- #@title lines render as collapsible Heading-2 form bars
- Ctrl+A in a cell output selects only that output, not the whole
notebook (the old behaviour ran notebook:select-all and was laggy)
- right activity bar hidden by default
- overrides.json: per-cell run button without auto-advance, labeled
Restart and Run All, windowing off so collapsing an output does not
snap to the cell top, news/update prompts suppressed
Studio and login branding: Unsloth favicon, page logo, and a dark
Unsloth login page that rotates through the curated Studio sloth
stickers (fail-soft to the logo).
Notebook organization and Colab compatibility (base image):
- categorized folder view built from relative symlinks mirroring the
README sections, rebuilt each boot; real .ipynb files never moved,
and the symlink tree is invisible to the sync state machine
- AMD-* notebooks shown only on an AMD/HIP host (autodetected)
- Docker-only strip of the Colab "Run all on Colab" intro sentence
from unedited notebooks (upstream notebooks unchanged)
- hoist %%capture above a leading #@title form so the cell runs
- the per-cell transformers-sidecar log is silent unless
UNSLOTH_ENABLE_LOGGING=1
Dependency pinning and naming: the curated notebook extras are pinned to
their resolved versions for reproducible rebuilds; decord is split into
its own fail-soft install (no aarch64 wheel). The lean base image is
renamed from :base to :core.
Adds tests/validate_studio_features.py, a static self-test for the
labextension plugins, overrides keys, and branding wiring.
97 lines
4.1 KiB
HTML
97 lines
4.1 KiB
HTML
{# Unsloth-branded JupyterLab login page. Overwrites jupyter_server's default
|
|
login.html (same overwrite pattern as the favicon/logo). Extends the stock
|
|
page.html so favicon (already the Unsloth icon) and form plumbing stay intact;
|
|
we override the title, hide the stock header, and render a dark centered card
|
|
matching the "Unsloth Dark" (Monokai) theme. The card logo reads
|
|
static/logo/logo.png, which the image build replaces with the Unsloth logo. #}
|
|
{% extends "page.html" %}
|
|
|
|
{% block title %}Unsloth{% endblock %}
|
|
|
|
{% block stylesheet %}
|
|
<style>
|
|
html, body {
|
|
background: hsl(70, 8%, 12%) !important;
|
|
color: hsl(60, 30%, 96%);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
}
|
|
/* Hide the stock top header (jupyter_server's index.css uses a higher-
|
|
specificity selector, so force it); the centered card carries the brand. */
|
|
#header, .header-bar { display: none !important; }
|
|
#site { display: flex; justify-content: center; align-items: flex-start; }
|
|
.unsloth-login-card {
|
|
margin-top: 11vh;
|
|
background: hsl(70, 8%, 18%);
|
|
border: 1px solid hsl(70, 8%, 28%);
|
|
border-radius: 12px;
|
|
padding: 38px 40px 32px;
|
|
width: 360px;
|
|
max-width: 90vw;
|
|
text-align: center;
|
|
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.45);
|
|
}
|
|
.unsloth-login-card img.logo { height: 72px; width: auto; margin-bottom: 14px; }
|
|
/* A random Unsloth Studio sloth sticker, shown like the Studio login screen. */
|
|
.unsloth-login-card img.sloth {
|
|
height: 104px; width: 104px; object-fit: contain;
|
|
margin: 2px auto 10px; display: block;
|
|
}
|
|
.unsloth-login-card h1 { font-size: 22px; margin: 0 0 4px; font-weight: 700; }
|
|
.unsloth-login-card p.sub { color: hsl(60, 8%, 64%); margin: 0 0 24px; font-size: 14px; }
|
|
.unsloth-login-card label {
|
|
display: block; text-align: left; font-size: 13px;
|
|
margin-bottom: 6px; color: hsl(60, 8%, 76%);
|
|
}
|
|
.unsloth-login-card input[type="password"] {
|
|
width: 100%; box-sizing: border-box; padding: 10px 12px;
|
|
border-radius: 8px; border: 1px solid hsl(70, 8%, 32%);
|
|
background: hsl(70, 8%, 13%); color: inherit; font-size: 14px; margin-bottom: 18px;
|
|
}
|
|
.unsloth-login-card input[type="password"]:focus {
|
|
outline: none; border-color: hsl(160, 55%, 48%);
|
|
}
|
|
.unsloth-login-card button {
|
|
width: 100%; padding: 10px 12px; border-radius: 8px; border: none;
|
|
background: hsl(160, 55%, 42%); color: #fff; font-weight: 600; font-size: 14px; cursor: pointer;
|
|
}
|
|
.unsloth-login-card button:hover { background: hsl(160, 55%, 36%); }
|
|
.unsloth-login-card .message { margin-top: 16px; font-size: 13px; }
|
|
.unsloth-login-card .message.error { color: hsl(0, 75%, 68%); }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block site %}
|
|
{# A different Unsloth Studio sloth sticker each visit (matches Studio's login).
|
|
The PNGs are copied into static/sloth/NN.png by the image build; if one is
|
|
missing the onerror handler falls back to the Unsloth logo so the page never
|
|
shows a broken image. #}
|
|
{% set sloths = [
|
|
"01.png", "02.png", "03.png", "04.png", "05.png", "06.png", "07.png",
|
|
"08.png", "09.png", "10.png", "11.png", "12.png", "13.png", "14.png",
|
|
"15.png", "16.png", "17.png", "18.png", "19.png", "20.png"
|
|
] %}
|
|
<div class="unsloth-login-card">
|
|
<img class="sloth" src='{{ static_url("sloth/" ~ (sloths | random)) }}'
|
|
onerror="this.onerror=null;this.className='logo';this.src='{{ static_url('logo/logo.png') }}';"
|
|
alt='Unsloth' />
|
|
<h1>Unsloth</h1>
|
|
<p class="sub">Sign in to JupyterLab</p>
|
|
{% if login_available %}
|
|
<form action="{{base_url}}login?next={{next}}" method="post">
|
|
{{ xsrf_form_html() | safe }}
|
|
<label for="password_input">
|
|
{% if token_available %}{% trans %}Password or token{% endtrans %}{% else %}{% trans %}Password{% endtrans %}{% endif %}
|
|
</label>
|
|
<input type="password" name="password" id="password_input" autofocus>
|
|
<button type="submit" id="login_submit">{% trans %}Log in{% endtrans %}</button>
|
|
</form>
|
|
{% endif %}
|
|
{% if message %}
|
|
{% for key in message %}
|
|
<div class="message {{key}}">{{ message[key] }}</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block script %}{% endblock %}
|