Add Mac OS support for Apple Silicon (#770)

* Made an install script and auto updates env for mac

* GPU sensors and initial training working for MAC. Still WIP.

* Switch dataloader to single threaded until I can work around some mac pickeling issues.

* Get quantization working on mac

* Fix mac exclusive imports so they don't break other builds.

* Add mac instructions to the UI
This commit is contained in:
Jaret Burkett 2026-03-30 09:37:47 -06:00 committed by GitHub
parent bc47fd6755
commit 171535833a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1072 additions and 71 deletions

2
.gitignore vendored
View File

@ -122,6 +122,8 @@ celerybeat.pid
# Environments # Environments
.env .env
.venv .venv
.python
.node
env/ env/
venv/ venv/
ENV/ ENV/

View File

@ -236,7 +236,7 @@ _Last updated: 2026-03-03 15:01 UTC_
## Installation ## Installation
Requirements: Requirements:
- python >3.10 - python >=3.10 (3.12 recommended)
- Nvidia GPU with enough ram to do what you need - Nvidia GPU with enough ram to do what you need
- python venv - python venv
- git - git
@ -269,6 +269,20 @@ pip install --no-cache-dir torch==2.9.1 torchvision==0.24.1 torchaudio==2.9.1 --
pip install -r requirements.txt pip install -r requirements.txt
``` ```
MacOS:
Experimental support for Silicon Macs is available. I do not have a Mac with enough RAM to fully test this
so please let me know if there are issues. There is a convience script to install and run on MacOS
locates at `./run_mac.zsh` that will install the dependencies locally and run the UI. To run this,
do the following:
```bash
git clone https://github.com/ostris/ai-toolkit.git
cd ai-toolkit
chmod +x run_mac.zsh
./run_mac.zsh
```
# AI Toolkit UI # AI Toolkit UI

View File

@ -39,11 +39,7 @@ import torch.nn.functional as F
from toolkit.unloader import unload_text_encoder from toolkit.unloader import unload_text_encoder
from PIL import Image from PIL import Image
from torchvision.transforms import functional as TF from torchvision.transforms import functional as TF
from toolkit.basic import flush
def flush():
torch.cuda.empty_cache()
gc.collect()
adapter_transforms = transforms.Compose([ adapter_transforms = transforms.Compose([

View File

@ -72,10 +72,7 @@ import hashlib
from toolkit.util.blended_blur_noise import get_blended_blur_noise from toolkit.util.blended_blur_noise import get_blended_blur_noise
from toolkit.util.get_model import get_model_class from toolkit.util.get_model import get_model_class
from toolkit.basic import flush
def flush():
torch.cuda.empty_cache()
gc.collect()
class BaseSDTrainProcess(BaseTrainProcess): class BaseSDTrainProcess(BaseTrainProcess):

166
run_mac.zsh Executable file
View File

@ -0,0 +1,166 @@
#!/usr/bin/env zsh
# Update-and-run script for macOS — portable Python 3.12 + PyTorch
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# ── Banner ─────────────────────────────────────────────────────────
echo ""
echo "\033[36m"
cat << 'BANNER'
_ ___ _____ _ _ _ _
/ \ |_ _| |_ _| ___ ___ | || | __(_)| |_
/ _ \ | | | | / _ \ / _ \| || |/ /| || __|
/ ___ \ | | | | | (_) || (_) | || < | || |_
/_/ \_\|___| |_| \___/ \___/|_||_|\_\|_| \__|
BANNER
echo "\033[0m"
echo "\033[90m macOS Setup & Launcher\033[0m"
echo ""
VENV_DIR="$SCRIPT_DIR/.venv"
PIP="$VENV_DIR/bin/pip"
PYTHON="$VENV_DIR/bin/python3"
PYTHON_VERSION="3.12.8"
RELEASE_TAG="20241219"
# --- Package versions (update these as needed) ---
NODE_VERSION="23.11.1"
TORCH_VERSION="2.11.0"
TORCHVISION_VERSION="0.26.0"
TORCHAUDIO_VERSION="2.11.0"
# Detect architecture
ARCH="$(uname -m)"
if [[ "$ARCH" == "arm64" ]]; then
PLATFORM="aarch64-apple-darwin"
elif [[ "$ARCH" == "x86_64" ]]; then
PLATFORM="x86_64-apple-darwin"
else
echo "Error: Unsupported architecture: $ARCH"
exit 1
fi
# ── 1. Download standalone Python if needed ─────────────────────────
PYTHON_DIR="$SCRIPT_DIR/.python"
PYTHON_BIN="$PYTHON_DIR/bin/python3"
if [[ ! -x "$PYTHON_BIN" ]]; then
TARBALL="cpython-${PYTHON_VERSION}+${RELEASE_TAG}-${PLATFORM}-install_only.tar.gz"
URL="https://github.com/indygreg/python-build-standalone/releases/download/${RELEASE_TAG}/${TARBALL}"
TMPDIR_DL="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_DL"' EXIT
echo "Downloading standalone Python ${PYTHON_VERSION} (${PLATFORM})..."
curl -fSL --progress-bar -o "$TMPDIR_DL/$TARBALL" "$URL"
echo "Extracting..."
tar -xzf "$TMPDIR_DL/$TARBALL" -C "$TMPDIR_DL"
# Move to permanent location (the archive extracts to a "python" folder)
rm -rf "$PYTHON_DIR"
mv "$TMPDIR_DL/python" "$PYTHON_DIR"
rm -rf "$TMPDIR_DL"
trap - EXIT
echo "Standalone Python installed to $PYTHON_DIR"
fi
# ── 2. Create venv if it doesn't exist ──────────────────────────────
if [[ ! -d "$VENV_DIR" ]]; then
echo "Creating virtual environment at $VENV_DIR..."
"$PYTHON_BIN" -m venv "$VENV_DIR"
echo "Virtual environment created."
fi
# ── 3. Download / update portable Node.js ──────────────────────────
NODE_DIR="$SCRIPT_DIR/.node"
NODE_BIN="$NODE_DIR/bin/node"
NEED_NODE=false
if [[ ! -x "$NODE_BIN" ]]; then
NEED_NODE=true
elif [[ "$("$NODE_BIN" --version 2>/dev/null)" != "v${NODE_VERSION}" ]]; then
echo "Node.js version mismatch (want v${NODE_VERSION}, have $("$NODE_BIN" --version))."
NEED_NODE=true
fi
if $NEED_NODE; then
if [[ "$ARCH" == "arm64" ]]; then
NODE_ARCH="arm64"
else
NODE_ARCH="x64"
fi
NODE_TARBALL="node-v${NODE_VERSION}-darwin-${NODE_ARCH}.tar.gz"
NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TARBALL}"
TMPDIR_DL="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_DL"' EXIT
echo "Downloading Node.js v${NODE_VERSION} (darwin-${NODE_ARCH})..."
curl -fSL --progress-bar -o "$TMPDIR_DL/$NODE_TARBALL" "$NODE_URL"
echo "Extracting..."
tar -xzf "$TMPDIR_DL/$NODE_TARBALL" -C "$TMPDIR_DL"
rm -rf "$NODE_DIR"
mv "$TMPDIR_DL/node-v${NODE_VERSION}-darwin-${NODE_ARCH}" "$NODE_DIR"
rm -rf "$TMPDIR_DL"
trap - EXIT
echo "Node.js v${NODE_VERSION} installed to $NODE_DIR"
else
echo "Node.js v${NODE_VERSION} is up to date."
fi
# ── 4. Install / update PyTorch packages ────────────────────────────
# Helper: returns 0 if the package is installed at the exact version
pkg_ok() {
local pkg="$1" want="$2"
local got
got="$("$PIP" show "$pkg" 2>/dev/null | awk '/^Version:/{print $2}')" || true
[[ "$got" == "$want" ]]
}
PKGS_TO_INSTALL=()
pkg_ok "torch" "$TORCH_VERSION" || PKGS_TO_INSTALL+=("torch==$TORCH_VERSION")
pkg_ok "torchvision" "$TORCHVISION_VERSION" || PKGS_TO_INSTALL+=("torchvision==$TORCHVISION_VERSION")
pkg_ok "torchaudio" "$TORCHAUDIO_VERSION" || PKGS_TO_INSTALL+=("torchaudio==$TORCHAUDIO_VERSION")
if (( ${#PKGS_TO_INSTALL[@]} )); then
echo "Installing / updating: ${PKGS_TO_INSTALL[*]}"
"$PIP" install "${PKGS_TO_INSTALL[@]}"
else
echo "PyTorch packages are up to date."
fi
# ── 5. Install / update requirements.txt ────────────────────────────
REQUIREMENTS="$SCRIPT_DIR/requirements.txt"
REQ_HASH_FILE="$VENV_DIR/.requirements_hash"
if [[ -f "$REQUIREMENTS" ]]; then
# Hash all requirements files (follows -r includes)
CURRENT_HASH="$(cat "$SCRIPT_DIR"/requirements*.txt 2>/dev/null | shasum -a 256 | awk '{print $1}')"
STORED_HASH=""
[[ -f "$REQ_HASH_FILE" ]] && STORED_HASH="$(cat "$REQ_HASH_FILE")"
if [[ "$CURRENT_HASH" != "$STORED_HASH" ]]; then
echo "Installing / updating requirements.txt..."
"$PIP" install -r "$REQUIREMENTS"
echo "$CURRENT_HASH" > "$REQ_HASH_FILE"
else
echo "Requirements are up to date."
fi
fi
# ── 6. Build and start the UI ───────────────────────────────────────
export PATH="$NODE_DIR/bin:$VENV_DIR/bin:$PATH"
echo ""
echo "Starting UI..."
cd "$SCRIPT_DIR/ui"
npm run build_and_start

View File

@ -9,7 +9,11 @@ def value_map(inputs, min_in, max_in, min_out, max_out):
def flush(garbage_collect=True): def flush(garbage_collect=True):
torch.cuda.empty_cache() if torch.cuda.is_available():
torch.cuda.empty_cache()
# if is mps, also clear the mps cache
if torch.backends.mps.is_available():
torch.mps.empty_cache()
if garbage_collect: if garbage_collect:
gc.collect() gc.collect()

View File

@ -7,6 +7,7 @@ import torch
import torchaudio import torchaudio
from toolkit.prompt_utils import PromptEmbeds from toolkit.prompt_utils import PromptEmbeds
from torchao.quantization.quant_primitives import _DTYPE_TO_BIT_WIDTH
ImgExt = Literal['jpg', 'png', 'webp'] ImgExt = Literal['jpg', 'png', 'webp']
@ -668,6 +669,12 @@ class ModelConfig:
self.qtype = "float8" self.qtype = "float8"
if self.layer_offloading and self.qtype_te == "qfloat8": if self.layer_offloading and self.qtype_te == "qfloat8":
self.qtype_te = "float8" self.qtype_te = "float8"
# Mac mps only works with torachao uint
if torch.backends.mps.is_available() and self.qtype == "qfloat8":
self.qtype = "int8"
if torch.backends.mps.is_available() and self.qtype_te == "qfloat8":
self.qtype_te = "int8"
# 0 is off and 1.0 is 100% of the layers # 0 is off and 1.0 is 100% of the layers
self.layer_offloading_transformer_percent = kwargs.get("layer_offloading_transformer_percent", 1.0) self.layer_offloading_transformer_percent = kwargs.get("layer_offloading_transformer_percent", 1.0)

View File

@ -29,6 +29,9 @@ import platform
def is_native_windows(): def is_native_windows():
return platform.system() == "Windows" and platform.release() != "2" return platform.system() == "Windows" and platform.release() != "2"
def is_macos():
return platform.system() == "Darwin"
if TYPE_CHECKING: if TYPE_CHECKING:
from toolkit.stable_diffusion_model import StableDiffusion from toolkit.stable_diffusion_model import StableDiffusion
@ -678,7 +681,7 @@ def get_dataloader_from_datasets(
dataloader_kwargs = {} dataloader_kwargs = {}
if is_native_windows(): if is_native_windows() or is_macos():
dataloader_kwargs['num_workers'] = 0 dataloader_kwargs['num_workers'] = 0
else: else:
dataloader_kwargs['num_workers'] = dataset_config_list[0].num_workers dataloader_kwargs['num_workers'] = dataset_config_list[0].num_workers

View File

@ -41,6 +41,7 @@ from torchvision.transforms import functional as TF
from toolkit.accelerator import get_accelerator, unwrap_model from toolkit.accelerator import get_accelerator, unwrap_model
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from toolkit.print import print_acc from toolkit.print import print_acc
from toolkit.basic import flush
if TYPE_CHECKING: if TYPE_CHECKING:
from toolkit.lora_special import LoRASpecialNetwork from toolkit.lora_special import LoRASpecialNetwork
@ -90,11 +91,6 @@ class BlankNetwork:
pass pass
def flush():
torch.cuda.empty_cache()
gc.collect()
UNET_IN_CHANNELS = 4 # Stable Diffusion の in_channels は 4 で固定。XLも同じ。 UNET_IN_CHANNELS = 4 # Stable Diffusion の in_channels は 4 で固定。XLも同じ。
# VAE_SCALE_FACTOR = 8 # 2 ** (len(vae.config.block_out_channels) - 1) = 8 # VAE_SCALE_FACTOR = 8 # 2 ** (len(vae.config.block_out_channels) - 1) = 8

View File

@ -70,6 +70,7 @@ from typing import TYPE_CHECKING
from toolkit.print import print_acc from toolkit.print import print_acc
from diffusers import FluxFillPipeline from diffusers import FluxFillPipeline
from transformers import AutoModel, AutoTokenizer, Gemma2Model, Qwen2Model, LlamaModel from transformers import AutoModel, AutoTokenizer, Gemma2Model, Qwen2Model, LlamaModel
from toolkit.basic import flush
if TYPE_CHECKING: if TYPE_CHECKING:
from toolkit.lora_special import LoRASpecialNetwork from toolkit.lora_special import LoRASpecialNetwork
@ -118,11 +119,6 @@ class BlankNetwork:
pass pass
def flush():
torch.cuda.empty_cache()
gc.collect()
UNET_IN_CHANNELS = 4 # Stable Diffusion の in_channels は 4 で固定。XLも同じ。 UNET_IN_CHANNELS = 4 # Stable Diffusion の in_channels は 4 で固定。XLも同じ。
# VAE_SCALE_FACTOR = 8 # 2 ** (len(vae.config.block_out_channels) - 1) = 8 # VAE_SCALE_FACTOR = 8 # 2 ** (len(vae.config.block_out_channels) - 1) = 8

View File

@ -8,6 +8,7 @@ from torchao.quantization.quant_api import (
quantize_ as torchao_quantize_, quantize_ as torchao_quantize_,
Float8WeightOnlyConfig, Float8WeightOnlyConfig,
UIntXWeightOnlyConfig, UIntXWeightOnlyConfig,
Int8WeightOnlyConfig
) )
from optimum.quanto import freeze from optimum.quanto import freeze
from tqdm import tqdm from tqdm import tqdm
@ -41,6 +42,7 @@ torchao_qtypes = {
"uint6": UIntXWeightOnlyConfig(torch.uint6), "uint6": UIntXWeightOnlyConfig(torch.uint6),
"uint7": UIntXWeightOnlyConfig(torch.uint7), "uint7": UIntXWeightOnlyConfig(torch.uint7),
"uint8": UIntXWeightOnlyConfig(torch.uint8), "uint8": UIntXWeightOnlyConfig(torch.uint8),
"int8": Int8WeightOnlyConfig(),
"float8": Float8WeightOnlyConfig(), "float8": Float8WeightOnlyConfig(),
} }

View File

@ -1,6 +1,13 @@
import type { NextConfig } from 'next'; import type { NextConfig } from 'next';
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
serverExternalPackages: ['macstats', 'osx-temperature-sensor'],
webpack: (config, { isServer }) => {
if (isServer) {
config.externals.push('osx-temperature-sensor', 'macstats');
}
return config;
},
devIndicators: { devIndicators: {
buildActivity: false, buildActivity: false,
}, },

661
ui/package-lock.json generated
View File

@ -18,8 +18,8 @@
"next": "^15.5.9", "next": "^15.5.9",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"prisma": "^6.3.1", "prisma": "^6.3.1",
"react": "^19.0.0", "react": "^19.2.0",
"react-dom": "^19.0.0", "react-dom": "^19.2.0",
"react-dropzone": "^14.3.5", "react-dropzone": "^14.3.5",
"react-global-hooks": "^1.3.5", "react-global-hooks": "^1.3.5",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
@ -42,6 +42,39 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"typescript": "^5" "typescript": "^5"
},
"optionalDependencies": {
"macstats": "^4.2.0"
}
},
"node_modules/@alcalzone/ansi-tokenize": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.5.tgz",
"integrity": "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==",
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"get-east-asian-width": "^1.3.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@ -1616,10 +1649,27 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/ansi-escapes": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
"integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
"license": "MIT",
"optional": true,
"dependencies": {
"environment": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "6.1.0", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -1628,9 +1678,10 @@
} }
}, },
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
"version": "6.2.1", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -1823,6 +1874,19 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/auto-bind": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz",
"integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/axios": { "node_modules/axios": {
"version": "1.12.2", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
@ -2225,6 +2289,69 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cli-boxes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
"integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
"integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
"license": "MIT",
"optional": true,
"dependencies": {
"restore-cursor": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz",
"integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
"license": "MIT",
"optional": true,
"dependencies": {
"slice-ansi": "^8.0.0",
"string-width": "^8.2.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/string-width": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz",
"integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==",
"license": "MIT",
"optional": true,
"dependencies": {
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/client-only": { "node_modules/client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@ -2333,6 +2460,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/code-excerpt": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
"integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
"license": "MIT",
"optional": true,
"dependencies": {
"convert-to-spaces": "^2.0.1"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -2476,6 +2616,16 @@
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
}, },
"node_modules/convert-to-spaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
"integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@ -2884,6 +3034,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/environment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/err-code": { "node_modules/err-code": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
@ -3271,6 +3434,19 @@
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
} }
}, },
"node_modules/get-east-asian-width": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
"integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -3584,6 +3760,165 @@
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ink": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/ink/-/ink-6.8.0.tgz",
"integrity": "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.4",
"ansi-escapes": "^7.3.0",
"ansi-styles": "^6.2.1",
"auto-bind": "^5.0.1",
"chalk": "^5.6.0",
"cli-boxes": "^3.0.0",
"cli-cursor": "^4.0.0",
"cli-truncate": "^5.1.1",
"code-excerpt": "^4.0.0",
"es-toolkit": "^1.39.10",
"indent-string": "^5.0.0",
"is-in-ci": "^2.0.0",
"patch-console": "^2.0.0",
"react-reconciler": "^0.33.0",
"scheduler": "^0.27.0",
"signal-exit": "^3.0.7",
"slice-ansi": "^8.0.0",
"stack-utils": "^2.0.6",
"string-width": "^8.1.1",
"terminal-size": "^4.0.1",
"type-fest": "^5.4.1",
"widest-line": "^6.0.0",
"wrap-ansi": "^9.0.0",
"ws": "^8.18.0",
"yoga-layout": "~3.2.1"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@types/react": ">=19.0.0",
"react": ">=19.0.0",
"react-devtools-core": ">=6.1.2"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"react-devtools-core": {
"optional": true
}
}
},
"node_modules/ink/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/ink/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"license": "MIT",
"optional": true
},
"node_modules/ink/node_modules/indent-string": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
"integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ink/node_modules/react-reconciler": {
"version": "0.33.0",
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz",
"integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==",
"license": "MIT",
"optional": true,
"dependencies": {
"scheduler": "^0.27.0"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^19.2.0"
}
},
"node_modules/ink/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC",
"optional": true
},
"node_modules/ink/node_modules/string-width": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz",
"integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==",
"license": "MIT",
"optional": true,
"dependencies": {
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ink/node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ink/node_modules/wrap-ansi/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/internmap": { "node_modules/internmap": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@ -3662,6 +3997,22 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-in-ci": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz",
"integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==",
"license": "MIT",
"optional": true,
"bin": {
"is-in-ci": "cli.js"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-lambda": { "node_modules/is-lambda": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
@ -3835,6 +4186,39 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/macstats": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/macstats/-/macstats-4.2.0.tgz",
"integrity": "sha512-+NJmIPjndK62WOwmu4Qbbgj5K84rTON7j3kBMOyFERsEztA7yM1ITyBRQQ/Y7cvRyYVeMi11C6KH7CFhIZlc2A==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"dependencies": {
"chalk": "^5.6.2",
"ink": "^6.4.0",
"nan": "^2.23.1",
"react": "^19.2.0"
},
"bin": {
"macstats": "bin/macstats"
}
},
"node_modules/macstats/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/make-error": { "node_modules/make-error": {
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@ -3949,6 +4333,16 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/mimic-response": { "node_modules/mimic-response": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@ -4193,6 +4587,13 @@
"thenify-all": "^1.0.0" "thenify-all": "^1.0.0"
} }
}, },
"node_modules/nan": {
"version": "2.26.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz",
"integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==",
"license": "MIT",
"optional": true
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.8", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
@ -4471,6 +4872,22 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"license": "MIT",
"optional": true,
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-map": { "node_modules/p-map": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
@ -4520,6 +4937,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/patch-console": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz",
"integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/path-is-absolute": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -4915,22 +5342,24 @@
} }
}, },
"node_modules/react": { "node_modules/react": {
"version": "19.0.0", "version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.0.0", "version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"scheduler": "^0.25.0" "scheduler": "^0.27.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^19.0.0" "react": "^19.2.4"
} }
}, },
"node_modules/react-dropzone": { "node_modules/react-dropzone": {
@ -5169,6 +5598,30 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/restore-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
"integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
"license": "MIT",
"optional": true,
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/restore-cursor/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC",
"optional": true
},
"node_modules/retry": { "node_modules/retry": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@ -5311,9 +5764,10 @@
"optional": true "optional": true
}, },
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.25.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==" "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.3", "version": "7.7.3",
@ -5466,6 +5920,39 @@
"simple-concat": "^1.0.0" "simple-concat": "^1.0.0"
} }
}, },
"node_modules/slice-ansi": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
"integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-styles": "^6.2.3",
"is-fullwidth-code-point": "^5.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"get-east-asian-width": "^1.3.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/smart-buffer": { "node_modules/smart-buffer": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@ -5599,6 +6086,29 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"escape-string-regexp": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/stack-utils/node_modules/escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/state-local": { "node_modules/state-local": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
@ -5680,11 +6190,12 @@
} }
}, },
"node_modules/strip-ansi": { "node_modules/strip-ansi": {
"version": "7.1.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1" "ansi-regex": "^6.2.2"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -5837,6 +6348,19 @@
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
}, },
"node_modules/tagged-tag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.17", "version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
@ -5933,6 +6457,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/terminal-size": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz",
"integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/text-decoder": { "node_modules/text-decoder": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
@ -6163,6 +6700,22 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/type-fest": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz",
"integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==",
"license": "(MIT OR CC0-1.0)",
"optional": true,
"dependencies": {
"tagged-tag": "^1.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.7.3", "version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
@ -6338,6 +6891,39 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/widest-line": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-6.0.0.tgz",
"integrity": "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==",
"license": "MIT",
"optional": true,
"dependencies": {
"string-width": "^8.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/widest-line/node_modules/string-width": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz",
"integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==",
"license": "MIT",
"optional": true,
"dependencies": {
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
@ -6428,6 +7014,28 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@ -6540,6 +7148,13 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/yoga-layout": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
"license": "MIT",
"optional": true
},
"node_modules/zip-stream": { "node_modules/zip-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",

View File

@ -22,8 +22,8 @@
"next": "^15.5.9", "next": "^15.5.9",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"prisma": "^6.3.1", "prisma": "^6.3.1",
"react": "^19.0.0", "react": "^19.2.0",
"react-dom": "^19.0.0", "react-dom": "^19.2.0",
"react-dropzone": "^14.3.5", "react-dropzone": "^14.3.5",
"react-global-hooks": "^1.3.5", "react-global-hooks": "^1.3.5",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
@ -47,5 +47,8 @@
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"typescript": "^5" "typescript": "^5"
}, },
"optionalDependencies": {
"macstats": "^4.2.0"
},
"prettier": "prettier-basic" "prettier": "prettier-basic"
} }

View File

@ -1,20 +1,57 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import si from 'systeminformation'; import si from 'systeminformation';
import { createRequire } from 'module';
import os from 'os';
import { CpuInfo } from '@/types'; import { CpuInfo } from '@/types';
const isMac = os.platform() === 'darwin';
export async function GET() { export async function GET() {
try { try {
const cpuInfoRaw = await si.cpu(); const cpuInfoRaw = await si.cpu();
const memoryData = await si.mem(); let cpuInfo: CpuInfo;
let cpuInfo: CpuInfo = {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`, if (isMac) {
cores: cpuInfoRaw.cores, try {
temperature: (await si.cpuTemperature()).main || 0, const nativeRequire = createRequire(import.meta.url);
totalMemory: memoryData.total / (1024 * 1024), const ms = nativeRequire('macstats') as any;
availableMemory: memoryData.available / (1024 * 1024), const ramData = ms.getRAMUsageSync();
freeMemory: memoryData.free / (1024 * 1024), const cpuData = ms.getCpuDataSync();
currentLoad: (await si.currentLoad()).currentLoad || 0,
}; cpuInfo = {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
cores: cpuInfoRaw.cores,
temperature: cpuData.temperature || 0,
totalMemory: ramData.total / (1024 * 1024),
availableMemory: ramData.free / (1024 * 1024),
freeMemory: ramData.free / (1024 * 1024),
currentLoad: (await si.currentLoad()).currentLoad || 0,
};
} catch {
// Fallback to systeminformation if macstats fails
const memoryData = await si.mem();
cpuInfo = {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
cores: cpuInfoRaw.cores,
temperature: (await si.cpuTemperature()).main || 0,
totalMemory: memoryData.total / (1024 * 1024),
availableMemory: memoryData.available / (1024 * 1024),
freeMemory: memoryData.free / (1024 * 1024),
currentLoad: (await si.currentLoad()).currentLoad || 0,
};
}
} else {
const memoryData = await si.mem();
cpuInfo = {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
cores: cpuInfoRaw.cores,
temperature: (await si.cpuTemperature()).main || 0,
totalMemory: memoryData.total / (1024 * 1024),
availableMemory: memoryData.available / (1024 * 1024),
freeMemory: memoryData.free / (1024 * 1024),
currentLoad: (await si.currentLoad()).currentLoad || 0,
};
}
return NextResponse.json(cpuInfo); return NextResponse.json(cpuInfo);
} catch (error) { } catch (error) {

View File

@ -1,15 +1,140 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { exec } from 'child_process'; import { exec, execSync } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { createRequire } from 'module';
import os from 'os'; import os from 'os';
const execAsync = promisify(exec); const execAsync = promisify(exec);
interface MacGpuResult {
name: string;
memUsed: number;
memTotal: number;
gpuLoad: number;
temperature: number;
fanSpeed: number;
powerDraw: number;
}
async function getMacGpuInfo(): Promise<MacGpuResult | null> {
try {
const memoryTotal = os.totalmem() / (1024 * 1024);
// Get GPU name and core count from system_profiler
let gpuName = 'Apple GPU';
try {
const spOut = execSync(
'system_profiler SPDisplaysDataType 2>/dev/null | grep -E "Chipset Model|Total Number of Cores"',
{ encoding: 'utf-8', timeout: 5000 },
);
const nameMatch = spOut.match(/Chipset Model:\s*(.+)/);
const coresMatch = spOut.match(/Total Number of Cores:\s*(\d+)/);
if (nameMatch) {
gpuName = nameMatch[1].trim();
if (coresMatch) {
gpuName += ` GPU (${coresMatch[1]} cores)`;
}
}
} catch {
// fallback to generic name
}
let temperature = 0;
let gpuLoad = 0;
let fanSpeed = 0;
let powerDraw = 0;
let memUsed = 0;
let memTotal = memoryTotal;
try {
// Use createRequire to hide from webpack static analysis so it doesn't fail on non-mac platforms
const nativeRequire = createRequire(import.meta.url);
const ms = nativeRequire('macstats') as any;
try {
const gpuData = ms.getGpuDataSync();
temperature = gpuData.temperature || 0;
gpuLoad = gpuData.usage || 0;
} catch {
// ignore
}
try {
const fanData = ms.getFanDataSync();
const fanKeys = Object.keys(fanData);
if (fanKeys.length > 0) {
fanSpeed = fanData[fanKeys[0]].rpm || 0;
}
} catch {
// ignore
}
try {
const powerData = ms.getPowerDataSync();
powerDraw = powerData.gpu || 0;
} catch {
// ignore
}
try {
const ramData = ms.getRAMUsageSync();
memUsed = ramData.used / (1024 * 1024);
memTotal = ramData.total / (1024 * 1024);
} catch {
// ignore
}
} catch (error) {
console.warn('macstats not available:', error);
}
return { name: gpuName, memUsed, memTotal, gpuLoad, temperature, fanSpeed, powerDraw };
} catch {
return null;
}
}
export async function GET() { export async function GET() {
try { try {
// Get platform // Get platform
const platform = os.platform(); const platform = os.platform();
const isWindows = platform === 'win32'; const isWindows = platform === 'win32';
const isMac = platform === 'darwin';
if (isMac) {
const macGpu = await getMacGpuInfo();
if (macGpu) {
return NextResponse.json({
hasNvidiaSmi: false,
isMac: true,
gpus: [
{
index: 0,
name: macGpu.name,
driverVersion: 'macOS',
temperature: Math.round(macGpu.temperature),
utilization: {
gpu: macGpu.gpuLoad,
memory: macGpu.memTotal > 0 ? Math.round((macGpu.memUsed / macGpu.memTotal) * 100) : 0,
},
memory: {
total: Math.round(macGpu.memTotal),
free: Math.round(macGpu.memTotal - macGpu.memUsed),
used: Math.round(macGpu.memUsed),
},
power: { draw: macGpu.powerDraw, limit: 0 },
clocks: { graphics: 0, memory: 0 },
fan: { speed: macGpu.fanSpeed },
},
],
});
}
return NextResponse.json({
hasNvidiaSmi: false,
isMac: true,
gpus: [],
error: 'Could not read Mac GPU stats',
});
}
// Check if nvidia-smi is available // Check if nvidia-smi is available
const hasNvidiaSmi = await checkNvidiaSmi(isWindows); const hasNvidiaSmi = await checkNvidiaSmi(isWindows);
@ -17,6 +142,7 @@ export async function GET() {
if (!hasNvidiaSmi) { if (!hasNvidiaSmi) {
return NextResponse.json({ return NextResponse.json({
hasNvidiaSmi: false, hasNvidiaSmi: false,
isMac: false,
gpus: [], gpus: [],
error: 'nvidia-smi not found or not accessible', error: 'nvidia-smi not found or not accessible',
}); });
@ -34,6 +160,7 @@ export async function GET() {
return NextResponse.json( return NextResponse.json(
{ {
hasNvidiaSmi: false, hasNvidiaSmi: false,
isMac: false,
gpus: [], gpus: [],
error: `Failed to fetch GPU stats: ${error instanceof Error ? error.message : String(error)}`, error: `Failed to fetch GPU stats: ${error instanceof Error ? error.message : String(error)}`,
}, },
@ -121,3 +248,4 @@ async function getGpuStats(isWindows: boolean) {
return gpus; return gpus;
} }

View File

@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { isMac } from '@/helpers/basic';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@ -28,7 +29,12 @@ export async function GET(request: Request) {
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = await request.json(); const body = await request.json();
const { id, name, job_config, gpu_ids } = body; const { id, name, job_config } = body;
let gpu_ids: string = body.gpu_ids;
if (isMac()) {
gpu_ids = "mps";
}
if (id) { if (id) {
// Update existing training // Update existing training

View File

@ -19,6 +19,7 @@ import SampleControlImage from '@/components/SampleControlImage';
import { FlipHorizontal2, FlipVertical2 } from 'lucide-react'; import { FlipHorizontal2, FlipVertical2 } from 'lucide-react';
import { handleModelArchChange } from './utils'; import { handleModelArchChange } from './utils';
import { IoFlaskSharp } from 'react-icons/io5'; import { IoFlaskSharp } from 'react-icons/io5';
import { isMac } from '@/helpers/basic';
type Props = { type Props = {
jobConfig: JobConfig; jobConfig: JobConfig;
@ -146,6 +147,8 @@ export default function SimpleJob({
return newQuantizationOptions; return newQuantizationOptions;
}, [modelArch]); }, [modelArch]);
const showGPUSelect = !isMac();
return ( return (
<> <>
<form <form
@ -171,13 +174,15 @@ export default function SimpleJob({
disabled={runId !== null} disabled={runId !== null}
required required
/> />
<SelectInput {showGPUSelect && (
label="GPU ID" <SelectInput
value={`${gpuIDs}`} label="GPU ID"
docKey="gpuids" value={`${gpuIDs}`}
onChange={value => setGpuIDs(value)} docKey="gpuids"
options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))} onChange={value => setGpuIDs(value)}
/> options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))}
/>
)}
{disableSections.includes('trigger_word') ? null : ( {disableSections.includes('trigger_word') ? null : (
<TextInput <TextInput
label="Trigger Word" label="Trigger Word"
@ -249,7 +254,7 @@ export default function SimpleJob({
onChange={value => setJobConfig(value, 'config.process[0].model.model_kwargs.match_target_res')} onChange={value => setJobConfig(value, 'config.process[0].model.model_kwargs.match_target_res')}
/> />
)} )}
{modelArch?.additionalSections?.includes('model.layer_offloading') && ( {modelArch?.additionalSections?.includes('model.layer_offloading') && !isMac() && (
<> <>
<Checkbox <Checkbox
label={ label={

View File

@ -1,3 +1,5 @@
'use client';
import { isMac } from '@/helpers/basic';
import { JobConfig, DatasetConfig, SliderConfig } from '@/types'; import { JobConfig, DatasetConfig, SliderConfig } from '@/types';
export const defaultDatasetConfig: DatasetConfig = { export const defaultDatasetConfig: DatasetConfig = {
@ -199,5 +201,9 @@ export const migrateJobConfig = (jobConfig: JobConfig): JobConfig => {
use_ui_logger: true, use_ui_logger: true,
}; };
} }
if (isMac()) {
jobConfig.config.process[0].device = 'mps';
}
return jobConfig; return jobConfig;
}; };

View File

@ -35,7 +35,7 @@ export default function TrainingForm() {
const [datasetOptions, setDatasetOptions] = useState<{ value: string; label: string }[]>([]); const [datasetOptions, setDatasetOptions] = useState<{ value: string; label: string }[]>([]);
const [showAdvancedView, setShowAdvancedView] = useState(false); const [showAdvancedView, setShowAdvancedView] = useState(false);
const [jobConfig, setJobConfig] = useNestedState<JobConfig>(objectCopy(defaultJobConfig)); const [jobConfig, setJobConfig] = useNestedState<JobConfig>(objectCopy(migrateJobConfig(defaultJobConfig)));
const [status, setStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle'); const [status, setStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);

View File

@ -7,6 +7,7 @@ import ConfirmModal from '@/components/ConfirmModal';
import { Suspense } from 'react'; import { Suspense } from 'react';
import AuthWrapper from '@/components/AuthWrapper'; import AuthWrapper from '@/components/AuthWrapper';
import DocModal from '@/components/DocModal'; import DocModal from '@/components/DocModal';
import os from 'os';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@ -21,12 +22,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
// Check if the AI_TOOLKIT_AUTH environment variable is set // Check if the AI_TOOLKIT_AUTH environment variable is set
const authRequired = process.env.AI_TOOLKIT_AUTH ? true : false; const authRequired = process.env.AI_TOOLKIT_AUTH ? true : false;
const platform = os.platform();
return ( return (
<html lang="en" className="dark"> <html lang="en" className="dark">
<head> <head>
<meta name="apple-mobile-web-app-title" content="AI-Toolkit" /> <meta name="apple-mobile-web-app-title" content="AI-Toolkit" />
</head> </head>
<body className={inter.className}> <body className={inter.className}>
<script dangerouslySetInnerHTML={{ __html: `window.server_platform = "${platform}";` }} />
<ThemeProvider> <ThemeProvider>
<AuthWrapper authRequired={authRequired}> <AuthWrapper authRequired={authRequired}>
<div className="flex h-screen bg-gray-950"> <div className="flex h-screen bg-gray-950">

View File

@ -98,7 +98,7 @@ const GpuMonitor: React.FC = () => {
); );
} }
if (!gpuData.hasNvidiaSmi) { if (!gpuData.hasNvidiaSmi && !gpuData.isMac) {
return ( return (
<div className="bg-yellow-900 border border-yellow-700 text-yellow-300 px-4 py-3 rounded relative" role="alert"> <div className="bg-yellow-900 border border-yellow-700 text-yellow-300 px-4 py-3 rounded relative" role="alert">
<strong className="font-bold">No NVIDIA GPUs detected!</strong> <strong className="font-bold">No NVIDIA GPUs detected!</strong>

View File

@ -14,12 +14,17 @@ interface JobOverviewProps {
} }
export default function JobOverview({ job }: JobOverviewProps) { export default function JobOverview({ job }: JobOverviewProps) {
const gpuIds = useMemo(() => job.gpu_ids.split(',').map(id => parseInt(id)), [job.gpu_ids]); const gpuIds = useMemo(() => {
if (job.gpu_ids === 'mps') {
return [0]; // For MPS, we can just return a single GPU ID since it's virtualized
}
return job.gpu_ids.split(',').map(id => parseInt(id));
}, [job.gpu_ids]);
const { log, setLog, status: statusLog, refresh: refreshLog } = useJobLog(job.id, 2000); const { log, setLog, status: statusLog, refresh: refreshLog } = useJobLog(job.id, 2000);
const logRef = useRef<HTMLDivElement>(null); const logRef = useRef<HTMLDivElement>(null);
// Track whether we should auto-scroll to bottom // Track whether we should auto-scroll to bottom
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true); const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
console.log('job.gpu_ids', job.gpu_ids);
const { gpuList, isGPUInfoLoaded } = useGPUInfo(gpuIds, 5000); const { gpuList, isGPUInfoLoaded } = useGPUInfo(gpuIds, 5000);
const { cpuInfo, isCPUInfoLoaded } = useCPUInfo(5000); const { cpuInfo, isCPUInfoLoaded } = useCPUInfo(5000);
const totalSteps = getTotalSteps(job); const totalSteps = getTotalSteps(job);

1
ui/src/helpers/basic.ts Normal file
View File

@ -0,0 +1 @@
export const isMac = () => (typeof window !== 'undefined' && (window as any).server_platform === 'darwin') || process.platform === 'darwin';

View File

@ -51,6 +51,7 @@ export interface CpuInfo {
export interface GPUApiResponse { export interface GPUApiResponse {
hasNvidiaSmi: boolean; hasNvidiaSmi: boolean;
isMac: boolean;
gpus: GpuInfo[]; gpus: GpuInfo[];
error?: string; error?: string;
} }