Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c9ff52b
docker: Colab-grade JupyterLab and Studio UX for the Blackwell image
danielhanchen Jun 25, 2026
7b5bb24
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 25, 2026
e496095
docker: address review feedback on the JupyterLab/Studio UX
danielhanchen Jun 26, 2026
50f7170
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 26, 2026
b963403
docker: publish lean image as :core and full image as :studio
danielhanchen Jun 26, 2026
dde7a26
docker: address second-round review feedback on the JupyterLab/Studio UX
danielhanchen Jun 26, 2026
2eba8a4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 26, 2026
30e7e09
Merge remote-tracking branch 'origin/docker-blackwell-build' into pr-…
danielhanchen Jun 26, 2026
9a53256
docker: keep Studio branding RUN free of comments inside the line con…
danielhanchen Jun 26, 2026
a56a7a8
Merge remote-tracking branch 'origin/docker-blackwell-build' into pr-…
danielhanchen Jun 26, 2026
2e4699c
Merge remote-tracking branch 'origin/docker-blackwell-build' into pr-…
danielhanchen Jun 26, 2026
38f1115
Merge remote-tracking branch 'origin/docker-blackwell-build' into pr-…
danielhanchen Jun 26, 2026
6fd9744
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 26, 2026
3fb8804
docker: AGPLv3 attribution + integrity guard for the Studio/JupyterLa…
danielhanchen Jun 27, 2026
d1a5b58
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 27, 2026
9cd1951
Merge remote-tracking branch 'origin/docker-blackwell-build' into pr-…
danielhanchen Jun 27, 2026
2482719
docker: address #6681 review round 2 (colab magics, output select, br…
danielhanchen Jun 27, 2026
0a8ae80
Merge remote-tracking branch 'origin/pr-jupyter-studio-ux' (pre-commi…
danielhanchen Jun 27, 2026
2c190aa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 27, 2026
04452eb
labext: pin JupyterLab extension deps; confirm.ps1 /login probe
danielhanchen Jun 29, 2026
67c0a8e
Merge remote-tracking branch 'origin/docker-blackwell-build' into HEAD
danielhanchen Jul 5, 2026
5d41a03
docker: categorize AMD/domain notebooks and wire the feature validati…
danielhanchen Jul 5, 2026
38df387
labext: use caret ranges so jlpm dedups JupyterLab/Lumino singletons
danielhanchen Jul 5, 2026
b558bc7
ci(studio-backend): trigger on docker/** so the JupyterLab feature va…
danielhanchen Jul 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docker/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,19 @@
!unsloth_run.py
!unsloth_sync_notebooks.sh
!unsloth_nb_content_sig.py
!unsloth_nb_view.py
!unsloth_nb_strip_colab.py
!unsloth_colab_compat.py
!jupyter
!jupyter/overrides.json
!jupyter/favicon.ico
!jupyter/logo.png
!jupyter/login.html
Comment on lines +25 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include the sticker installer in the Docker build context

When building Dockerfile.studio with docker/ as the context, this allowlist still excludes jupyter/install_sloth_stickers.py because the file is not whitelisted after the top-level ** ignore. The new COPY jupyter/install_sloth_stickers.py ... in the studio Dockerfile therefore fails with a missing source before the image can build; add an exception for that helper alongside the other jupyter/ assets.

Useful? React with 👍 / 👎.

!jupyter/unsloth_labext
!jupyter/unsloth_labext/package.json
!jupyter/unsloth_labext/tsconfig.json
!jupyter/unsloth_labext/.yarnrc.yml
!jupyter/unsloth_labext/src
!jupyter/unsloth_labext/src/**
!jupyter/unsloth_labext/style
!jupyter/unsloth_labext/style/**
29 changes: 22 additions & 7 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -285,18 +285,30 @@ RUN set -eux \
# omegaconf TTS families + both NeMo-Gym RL notebooks' config objects
# einx TTS codec tensor-rearrange (Llasa / Oute / Spark TTS)
# librosa Whisper audio feature extraction (pairs with soundfile + torchcodec)
# decord ERNIE-VL vision notebook video decode
# ftfy Oute TTS text normalisation
# decord (ERNIE-VL video decode) is installed separately below: it ships no
# aarch64 wheel, so a hard install here would break the arm64 build.
# librosa pulls numba/soxr/audioread; numba is already pinned >=0.65 (numpy 2.4
# compatible) by the vLLM pass, so the resolve must NOT move torch/numpy/numba --
# the assertion below fails the build loudly if it did.
# Pinned (==) to the resolved, tested versions for reproducible rebuilds -- the
# same convention as the cu128 core (torch/torchvision/torchaudio). Bump these
# deliberately, not silently on the next build. Transitive deps of these are
# captured by the full venv lockfile (docker/freeze.sh -> requirements.lock.txt).
RUN ${VENV}/bin/uv pip install \
--python ${VENV}/bin/python \
jupyterlab notebook ipywidgets matplotlib \
soundfile evaluate jiwer tensorboard langid easydict protobuf \
omegaconf einx librosa decord ftfy \
"jupyterlab==4.6.0" "notebook==7.6.0" "ipywidgets==8.1.8" "matplotlib==3.11.0" \
"soundfile==0.14.0" "evaluate==0.4.6" "jiwer==4.0.0" "tensorboard==2.20.0" \
"langid==1.1.6" "easydict==1.13" "protobuf==6.33.6" \
"omegaconf==2.3.1" "einx==0.4.3" "librosa==0.11.0" "ftfy==6.3.1" \
&& ${VENV}/bin/python -c "import torch, numpy, numba; from packaging.version import Version; assert torch.__version__.startswith('2.10.0'), torch.__version__; assert Version(numpy.__version__) >= Version('2.3'), numpy.__version__; assert Version(numba.__version__) >= Version('0.65'), numba.__version__; print('notebook-deps pins OK:', torch.__version__, numpy.__version__, numba.__version__)"

# decord (ERNIE-VL video decode) publishes wheels only for x86_64 / win_amd64,
# so install it on its own and fail-soft: amd64 gets it; on arm64 the ERNIE-VL
# video path is skipped rather than breaking the whole image build.
RUN ${VENV}/bin/uv pip install --python ${VENV}/bin/python "decord==0.6.0" \
|| echo ">> decord skipped (no matching wheel for ${TARGETARCH:-amd64}); ERNIE-VL video decode unavailable"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep decord mandatory on amd64 builds

This fallback is unconditional, so on amd64 a transient resolver failure or incompatible decord wheel still produces a successful image without the dependency, even though the ERNIE-VL video notebooks expect it and the comment says amd64 gets it. Restrict the fail-soft path to non-amd64 or add an amd64 import check so supported images fail instead of silently dropping video decode.

Useful? React with 👍 / 👎.


# Audio decode out of the box: the TTS/STT notebooks feed datasets' Audio
# features, which decode through torchcodec. Three traps, all defended:
# * version pairing: torchcodec 0.10 pairs with torch 2.10 (newer builds
Expand Down Expand Up @@ -647,19 +659,22 @@ RUN mkdir -p ${HF_HOME} ${TRITON_CACHE_DIR}
# * unsloth-run: headless `unsloth-run <notebook|url>` that auto-picks the
# sidecar and executes every cell -- the robust driven path.
# ---------------------------------------------------------------------------
COPY unsloth_nb_compat.py unsloth_pip_shim.py unsloth_ipython_startup.py unsloth_run.py unsloth_sync_notebooks.sh unsloth_nb_content_sig.py /opt/unsloth-nb/
COPY unsloth_nb_compat.py unsloth_pip_shim.py unsloth_ipython_startup.py unsloth_run.py unsloth_sync_notebooks.sh unsloth_nb_content_sig.py unsloth_nb_view.py unsloth_nb_strip_colab.py unsloth_colab_compat.py /opt/unsloth-nb/
RUN set -eux \
&& SP=/opt/unsloth-venv/lib/python${PYTHON_VERSION}/site-packages \
&& cp /opt/unsloth-nb/unsloth_nb_compat.py "$SP/unsloth_nb_compat.py" \
&& chmod +x /opt/unsloth-nb/unsloth_pip_shim.py /opt/unsloth-nb/unsloth_run.py /opt/unsloth-nb/unsloth_sync_notebooks.sh /opt/unsloth-nb/unsloth_nb_content_sig.py \
&& cp /opt/unsloth-nb/unsloth_colab_compat.py "$SP/unsloth_colab_compat.py" \
&& chmod +x /opt/unsloth-nb/unsloth_pip_shim.py /opt/unsloth-nb/unsloth_run.py /opt/unsloth-nb/unsloth_sync_notebooks.sh /opt/unsloth-nb/unsloth_nb_content_sig.py /opt/unsloth-nb/unsloth_nb_view.py /opt/unsloth-nb/unsloth_nb_strip_colab.py \
&& mkdir -p /opt/unsloth-nb/bin \
&& for t in pip pip3 uv; do ln -sf /opt/unsloth-nb/unsloth_pip_shim.py /opt/unsloth-nb/bin/$t; done \
&& ln -sf /opt/unsloth-nb/unsloth_run.py /usr/local/bin/unsloth-run \
&& ln -sf /opt/unsloth-nb/unsloth_sync_notebooks.sh /usr/local/bin/unsloth-sync-notebooks \
&& ln -sf /opt/unsloth-nb/unsloth_nb_content_sig.py /usr/local/bin/unsloth-nb-content-sig \
&& ln -sf /opt/unsloth-nb/unsloth_nb_view.py /usr/local/bin/unsloth-nb-view \
&& ln -sf /opt/unsloth-nb/unsloth_nb_strip_colab.py /usr/local/bin/unsloth-nb-strip-colab \
&& mkdir -p /root/.ipython/profile_default/startup \
&& cp /opt/unsloth-nb/unsloth_ipython_startup.py /root/.ipython/profile_default/startup/00-unsloth-nb.py \
&& /opt/unsloth-venv/bin/python -c "import sys, glob; sys.path.insert(0, '$SP'); import unsloth_nb_compat; print('nb-compat OK; baked sidecars:', sorted(glob.glob('/opt/unsloth-venv/tf-sidecars/t_*')))"
&& /opt/unsloth-venv/bin/python -c "import sys, glob; sys.path.insert(0, '$SP'); import unsloth_nb_compat, unsloth_colab_compat; print('nb-compat OK; baked sidecars:', sorted(glob.glob('/opt/unsloth-venv/tf-sidecars/t_*')))"
# Shim dir AHEAD of the venv bin so `!pip`/`!uv` resolve to the shim, not the real tool.
ENV PATH=/opt/unsloth-nb/bin:${PATH}

Expand Down
60 changes: 57 additions & 3 deletions docker/Dockerfile.studio
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Full Unsloth image: base training stack + Studio + JupyterLab + sshd.
#
# This is the image published as docker.io/unsloth/unsloth:latest. It layers
# Unsloth Studio on top of the lean base image (Dockerfile, published under
# the `base` tags) and runs the same service trio as the previous production
# This is the image published as docker.io/unsloth/unsloth:studio (and the
# default :latest). It layers Unsloth Studio on top of the lean core image
# (Dockerfile, published under the `core` tags) and runs the same service trio
# as the previous production
# image: Studio on 8000, JupyterLab on 8888, key-only sshd on 22.
#
# Build (local):
Expand All @@ -28,6 +29,22 @@
# images always ship the same stack.

ARG BASE_IMAGE=unsloth-blackwell:test

# --- builder stage: prebuild the Unsloth JupyterLab extension -----------------
# Builds the named "Unsloth Dark" (Monokai) theme + the Colab-style Down/Up
# cell-navigation keymap. Node lives ONLY in this throwaway stage; the final
# image copies just the prebuilt static labextension, so the runtime stays
# Node-free. Uses the base image's bundled jlpm + jupyterlab (version-matched).
FROM ${BASE_IMAGE} AS labext-builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends nodejs npm git \
&& rm -rf /var/lib/apt/lists/*
Comment on lines +43 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install a Node version that can build JupyterLab 4.6

On the Ubuntu 24.04 CUDA base, apt-get install nodejs resolves to Node 18, but the JupyterLab 4.6 JavaScript packages used by the next jlpm install declare a Node >=20 engine. In a clean studio build this makes the labextension builder fail before producing /opt/labext-src/unsloth-jupyterlab/labextension; install/pin a Node 20+ source instead of the distro nodejs package.

Useful? React with 👍 / 👎.

COPY jupyter/unsloth_labext /opt/labext-src
RUN cd /opt/labext-src \
&& /opt/unsloth-venv/bin/jlpm install \
&& /opt/unsloth-venv/bin/jlpm build:prod

FROM ${BASE_IMAGE}

# Studio source ref to clone. Defaults to `main`, but a CI publish pipeline
Expand Down Expand Up @@ -170,6 +187,43 @@ COPY fetch_llama_prebuilt.py /usr/local/lib/unsloth/fetch_llama_prebuilt.py
# Optional public Cloudflare tunnel for JupyterLab (UNSLOTH_JUPYTER_CLOUDFLARE=1,
# or `unsloth-jupyter-tunnel --force`); supervisord runs it as jupyter-cloudflare.
COPY unsloth_jupyter_tunnel.sh /usr/local/bin/unsloth-jupyter-tunnel
# JupyterLab defaults baked for every container: the named "Unsloth Dark"
# (Monokai) theme with adaptive light/dark by system preference, a per-cell run
# button that does NOT auto-advance, a labeled "Restart & Run All", windowing
# disabled so collapsing a long output does not snap to the cell top,
# ArrowDown/Up jumping to the TOP of the next/previous cell, and the official
# Jupyter "get notified about news" prompt suppressed (fetchNews/checkForUpdates
# off). overrides.json is the system-wide settings override (read from the base
# venv's share/jupyter/lab/settings); the theme + keymap + Unsloth top-bar logo
# ship as the prebuilt labextension built in the labext-builder stage above.
COPY jupyter/overrides.json /opt/unsloth-venv/share/jupyter/lab/settings/overrides.json
COPY --from=labext-builder /opt/labext-src/unsloth-jupyterlab/labextension /opt/unsloth-venv/share/jupyter/labextensions/unsloth-jupyterlab
# Unsloth branding (all served by jupyter_server, so applied to its site-packages
# the same way): replace the browser-tab favicon and the page logo with the
# Unsloth logo, and brand the login screen (dark Unsloth-themed login.html).
# Also disable + lock the stock top-left Jupyter logo plugin so the Unsloth logo
# widget shipped by the labextension is the only one rendered in the top bar
# (lock keeps users from re-enabling it in the UI).
COPY jupyter/favicon.ico /tmp/unsloth-branding/favicon.ico
COPY jupyter/logo.png /tmp/unsloth-branding/logo.png
COPY jupyter/login.html /tmp/unsloth-branding/login.html
COPY jupyter/install_sloth_stickers.py /tmp/unsloth-branding/install_sloth_stickers.py
RUN JS="$(/opt/unsloth-venv/bin/python -c 'import os, jupyter_server; print(os.path.dirname(jupyter_server.__file__))')" \
&& for n in favicon.ico favicon-notebook.ico favicon-file.ico favicon-terminal.ico; do \
cp /tmp/unsloth-branding/favicon.ico "${JS}/static/favicons/${n}"; \
done \
&& cp /tmp/unsloth-branding/logo.png "${JS}/static/logo/logo.png" \
&& cp /tmp/unsloth-branding/login.html "${JS}/templates/login.html" \
# Copy the curated Studio sloth stickers the login page rotates through into
# jupyter_server's static dir (sloth/NN.png). Fail-soft: if the Studio
# public folder ever moves, login.html's onerror falls back to the logo.
&& /opt/unsloth-venv/bin/python /tmp/unsloth-branding/install_sloth_stickers.py \
--src "${UNSLOTH_STUDIO_HOME}/src/studio/frontend/public/Sloth emojis" \
--dest "${JS}/static/sloth" \
|| echo ">> sloth stickers not installed (login falls back to the Unsloth logo)" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep required branding failures fatal

Because this || is attached to the entire preceding && chain, any required branding step above it, such as resolving JS or copying the favicon/logo/login template, is converted into the optional sticker warning and the build continues. If a pinned Jupyter Server layout changes, this would publish a partially branded or broken login page instead of failing the image build; group only the sticker installer with the fallback so earlier failures remain fatal.

Useful? React with 👍 / 👎.

&& rm -rf /tmp/unsloth-branding \
&& /opt/unsloth-venv/bin/jupyter labextension disable @jupyterlab/application-extension:logo \
&& /opt/unsloth-venv/bin/jupyter labextension lock @jupyterlab/application-extension:logo
RUN chmod +x /usr/local/bin/unsloth-studio-launch \
/usr/local/bin/unsloth-studio-update \
/usr/local/bin/unsloth-llama-update \
Expand Down
2 changes: 1 addition & 1 deletion docker/docker_confirm.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

$ErrorActionPreference = "Continue"
$IMAGE = if ($env:IMAGE) { $env:IMAGE } else { "unsloth/unsloth:latest" }
$BASE_IMAGE = if ($env:BASE_IMAGE) { $env:BASE_IMAGE } else { "unsloth/unsloth:base" }
$BASE_IMAGE = if ($env:BASE_IMAGE) { $env:BASE_IMAGE } else { "unsloth/unsloth:core" }
$GPUS = if ($env:GPUS) { $env:GPUS } else { "auto" }
$PORT_STUDIO = if ($env:PORT_STUDIO) { $env:PORT_STUDIO } else { 18000 }
$PORT_JUPYTER = if ($env:PORT_JUPYTER) { $env:PORT_JUPYTER } else { 18888 }
Expand Down
4 changes: 2 additions & 2 deletions docker/docker_confirm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
# Studio chat / Jupyter / GGUF tooling still validate.
#
# Env overrides: IMAGE (default unsloth/unsloth:latest)
# BASE_IMAGE (default unsloth/unsloth:base)
# BASE_IMAGE (default unsloth/unsloth:core)
# GPUS=all|none|0|0,1 (default: auto-detect)
# PORT_STUDIO=18000 PORT_JUPYTER=18888
# WORK=~/unsloth_docker_test (logs)
Expand All @@ -33,7 +33,7 @@
set -uo pipefail

IMAGE="${IMAGE:-unsloth/unsloth:latest}"
BASE_IMAGE="${BASE_IMAGE:-unsloth/unsloth:base}"
BASE_IMAGE="${BASE_IMAGE:-unsloth/unsloth:core}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish the tag used by confirmation scripts

This default now pulls unsloth/unsloth:core, but the in-repo publish workflow still creates only :base and base-* tags (.github/workflows/docker-publish.yml lines 228-238 and 432-435) and never creates a :core tag. On fresh user machines the confirmation scripts therefore fail in the pull phase unless BASE_IMAGE is overridden; update the workflow tags too, or keep the scripts on the published tag.

Useful? React with 👍 / 👎.

GPUS="${GPUS:-auto}"
PORT_STUDIO="${PORT_STUDIO:-18000}"
PORT_JUPYTER="${PORT_JUPYTER:-18888}"
Expand Down
4 changes: 3 additions & 1 deletion docker/fetch_llama_prebuilt.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ def resolve_latest_tag(repo: str) -> str:
final_url = response.geturl()
marker = "/releases/tag/"
if marker not in final_url:
raise SystemExit(f"FAIL: could not resolve latest release of {repo} (landed on {final_url})")
raise SystemExit(
f"FAIL: could not resolve latest release of {repo} (landed on {final_url})"
)
return final_url.rsplit(marker, 1)[1].strip("/")


Expand Down
Binary file added docker/jupyter/favicon.ico
Binary file not shown.
78 changes: 78 additions & 0 deletions docker/jupyter/install_sloth_stickers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Install the Unsloth Studio sloth stickers for the JupyterLab login screen.

The branded login page (login.html) shows a different sloth sticker on each
visit, the same curated set Studio offers as profile avatars. The PNGs live in
the Studio frontend (`studio/frontend/public/Sloth emojis/`), which is present
in the studio image after install.sh runs. This copies the curated subset into
jupyter_server's static dir as `sloth/01.png .. sloth/20.png` so the template
can reference stable, space-free, auth-free URLs via `static_url(...)`.

Usage:
install_sloth_stickers.py --src "<Sloth emojis dir>" --dest "<static>/sloth"

Fail-soft: a missing source file is skipped (login.html's onerror falls back to
the Unsloth logo), and the script still exits 0 as long as at least one sticker
was installed. Stdlib only.
"""

import argparse
import os
import shutil
import sys

# Curated, in display order -> NN.png. Mirrors Studio's SLOTH_AVATARS
# (frontend/src/features/profile/sloth-avatars.ts): the square, low-whitespace
# stickers that frame cleanly. Kept in sync by hand; missing names are skipped.
CURATED = [
"large sloth yay.png",
"large sloth heart.png",
"large sloth wave.png",
"large sloth thumbs.png",
"large sloth cheeky.png",
"large sloth glasses.png",
"large sloth fire.png",
"large sloth drink.png",
"large sloth sad.png",
"Large sloth Question mark.png",
"sloth shy large.png",
"sloth shock large.png",
"sloth sir large.png",
"sloth huglove large.png",
"sloth headphones.png",
"sloth pc square.png",
"sloth on phone.png",
"sloth magnify final.png",
"Sloth loca pc.png",
"UnSloth GPU Front square.png",
]


def main() -> int:
parser = argparse.ArgumentParser(description = __doc__)
parser.add_argument("--src", required = True, help = "Studio 'Sloth emojis' dir")
parser.add_argument("--dest", required = True, help = "output dir (static/sloth)")
args = parser.parse_args()

os.makedirs(args.dest, exist_ok = True)
installed = 0
for index, name in enumerate(CURATED, start = 1):
source = os.path.join(args.src, name)
target = os.path.join(args.dest, "%02d.png" % index)
if not os.path.isfile(source):
print(" skip (missing): %s" % name)
continue
try:
shutil.copyfile(source, target)
installed += 1
except OSError as error:
print(" skip (%s): %s" % (error, name))

print("installed %d/%d sloth stickers into %s" % (installed, len(CURATED), args.dest))
# Non-fatal: the login page degrades to the logo if none were installed, but
# a totally empty copy usually means a wrong --src, so signal that.
return 0 if installed else 1


if __name__ == "__main__":
sys.exit(main())
97 changes: 97 additions & 0 deletions docker/jupyter/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{# 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 %}
Binary file added docker/jupyter/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading