unsloth/docker/jupyter/login.html
Daniel Han c9ff52ba04 docker: Colab-grade JupyterLab and Studio UX for the Blackwell image
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.
2026-06-25 16:22:41 +00:00

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 %}