diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b6172a29e..67d95e1ca 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -49,7 +49,7 @@ body: attributes: label: nanobot Version description: Run `nanobot --version` or `pip show nanobot-ai` - placeholder: e.g., 0.1.5 + placeholder: e.g., 0.2.0 validations: required: true diff --git a/README.md b/README.md index ccc854fa6..7efdcf886 100644 --- a/README.md +++ b/README.md @@ -214,10 +214,9 @@ nanobot agent - Want to run nanobot in chat apps like Telegram, Discord, WeChat or Feishu? See [Chat Apps](./docs/chat-apps.md) - Want Docker or Linux service deployment? See [Deployment](./docs/deployment.md) -## ๐Ÿงช WebUI (Development) +## ๐ŸŒ WebUI -> [!NOTE] -> The WebUI development workflow currently requires a source checkout and is not yet shipped together with the official packaged release. See [WebUI Document](./webui/README.md) for full WebUI development docs and build steps. +The WebUI ships **inside the published wheel** โ€” no extra build step. Just enable the WebSocket channel and open it in your browser.

nanobot webui preview @@ -235,13 +234,12 @@ nanobot agent nanobot gateway ``` -**3. Start the webui dev server** +**3. Open the WebUI** -```bash -cd webui -bun install -bun run dev -``` +Visit [`http://127.0.0.1:8765`](http://127.0.0.1:8765) in your browser. To open it from another device on your LAN, see [WebUI docs โ†’ LAN access](./webui/README.md#access-from-another-device-lan). + +> [!TIP] +> Working on the WebUI itself? Check out [`webui/README.md`](./webui/README.md) for the Vite dev server (HMR) workflow. ## ๐Ÿ—๏ธ Architecture diff --git a/docs/README.md b/docs/README.md index 56b8dab2f..7ac873bd1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ Start here for setup, everyday usage, and deployment. | Agent social network | [`agent-social-network.md`](./agent-social-network.md) | Join external agent communities from nanobot | | Configuration | [`configuration.md`](./configuration.md) | Providers, tools, channels, MCP, and runtime settings | | Image generation | [`image-generation.md`](./image-generation.md) | Configure image providers, WebUI image mode, and generated artifacts | +| WebUI | [`../webui/README.md`](../webui/README.md) | Open the bundled browser UI; LAN access; Vite dev server for contributors | | Multiple instances | [`multiple-instances.md`](./multiple-instances.md) | Run isolated bots with separate configs and workspaces | | CLI reference | [`cli-reference.md`](./cli-reference.md) | Core CLI commands and common entrypoints | | In-chat commands | [`chat-commands.md`](./chat-commands.md) | Slash commands and periodic task behavior | diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 000000000..28dbcd09a --- /dev/null +++ b/hatch_build.py @@ -0,0 +1,101 @@ +"""Hatch build hook that bundles the webui (Vite) into nanobot/web/dist. + +Triggered automatically by `python -m build` (and any other hatch-driven build) +so published wheels and sdists ship a fresh webui without requiring developers +to remember `cd webui && bun run build` beforehand. + +Behaviour: + +- Skips for editable installs (`pip install -e .`). Editable mode is for Python + development; webui contributors use `cd webui && bun run dev` (Vite HMR) and + do not need a packaged `dist/`. +- No-op when `webui/package.json` is absent (e.g. installing from an sdist that + already contains a prebuilt `nanobot/web/dist/`). +- Skips when `NANOBOT_SKIP_WEBUI_BUILD=1` is set. +- Skips when `nanobot/web/dist/index.html` already exists, unless + `NANOBOT_FORCE_WEBUI_BUILD=1` is set. +- Uses `bun` when available, otherwise falls back to `npm`. The chosen tool + performs `install` followed by `run build`. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +from pathlib import Path + +from hatchling.builders.hooks.plugin.interface import BuildHookInterface + + +class WebUIBuildHook(BuildHookInterface): + PLUGIN_NAME = "webui-build" + + def initialize(self, version: str, build_data: dict) -> None: # noqa: D401 + root = Path(self.root) + webui_dir = root / "webui" + package_json = webui_dir / "package.json" + dist_dir = root / "nanobot" / "web" / "dist" + index_html = dist_dir / "index.html" + + # `pip install -e .` builds an editable wheel; skip the (slow) webui + # bundle since editable installs target Python development and webui + # work uses `bun run dev` instead. + if self.target_name == "wheel" and version == "editable": + self.app.display_info( + "[webui-build] skipped for editable install " + "(use `cd webui && bun run build` to bundle webui manually)" + ) + return + + if os.environ.get("NANOBOT_SKIP_WEBUI_BUILD") == "1": + self.app.display_info("[webui-build] skipped via NANOBOT_SKIP_WEBUI_BUILD=1") + return + + if not package_json.is_file(): + self.app.display_info( + "[webui-build] no webui/ source tree, assuming prebuilt nanobot/web/dist/" + ) + return + + force = os.environ.get("NANOBOT_FORCE_WEBUI_BUILD") == "1" + if index_html.is_file() and not force: + self.app.display_info( + f"[webui-build] reusing existing build at {dist_dir} " + "(set NANOBOT_FORCE_WEBUI_BUILD=1 to rebuild)" + ) + return + + runner = self._pick_runner() + if runner is None: + raise RuntimeError( + "[webui-build] neither `bun` nor `npm` is available on PATH; " + "install one or set NANOBOT_SKIP_WEBUI_BUILD=1 to bypass." + ) + + self.app.display_info(f"[webui-build] using {runner} to build webui") + self._run([runner, "install"], cwd=webui_dir) + self._run([runner, "run", "build"], cwd=webui_dir) + + if not index_html.is_file(): + raise RuntimeError( + f"[webui-build] build finished but {index_html} is missing; " + "check webui/vite.config.ts outDir." + ) + self.app.display_info(f"[webui-build] webui ready at {dist_dir}") + + @staticmethod + def _pick_runner() -> str | None: + for candidate in ("bun", "npm"): + if shutil.which(candidate): + return candidate + return None + + def _run(self, cmd: list[str], *, cwd: Path) -> None: + self.app.display_info(f"[webui-build] $ {' '.join(cmd)} (cwd={cwd})") + try: + subprocess.run(cmd, cwd=cwd, check=True) + except subprocess.CalledProcessError as exc: + raise RuntimeError( + f"[webui-build] command failed ({exc.returncode}): {' '.join(cmd)}" + ) from exc diff --git a/nanobot/__init__.py b/nanobot/__init__.py index e6fdbf0ba..8ab213a33 100644 --- a/nanobot/__init__.py +++ b/nanobot/__init__.py @@ -21,7 +21,7 @@ def _resolve_version() -> str: return _pkg_version("nanobot-ai") except PackageNotFoundError: # Source checkouts often import nanobot without installed dist-info. - return _read_pyproject_version() or "0.1.5.post3" + return _read_pyproject_version() or "0.2.0" __version__ = _resolve_version() diff --git a/nanobot/web/__init__.py b/nanobot/web/__init__.py index 7a08932f6..36ee3e934 100644 --- a/nanobot/web/__init__.py +++ b/nanobot/web/__init__.py @@ -1,6 +1,8 @@ """Embedded web UI assets. -The ``dist/`` subdirectory is populated by ``cd webui && bun run build`` and -is shipped in the wheel; it stays empty in source checkouts until that command -has been run. +The ``dist/`` subdirectory holds the production WebUI bundle served by the +gateway. It is shipped inside the published wheel and is rebuilt automatically +by the ``webui-build`` Hatch hook during ``python -m build``. In an editable +source checkout it stays empty until you run ``cd webui && bun run build`` +(or use the Vite dev server at ``cd webui && bun run dev``). """ diff --git a/pyproject.toml b/pyproject.toml index 16ed57dd2..eaf57a2ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "nanobot-ai" -version = "0.1.5.post3" +version = "0.2.0" description = "A lightweight personal AI assistant framework" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" @@ -121,12 +121,22 @@ build-backend = "hatchling.build" [tool.hatch.metadata] allow-direct-references = true +[tool.hatch.build.hooks.custom] +# Implementation lives in the conventional `hatch_build.py` at the repo root. + [tool.hatch.build] include = [ "nanobot/**/*.py", "nanobot/templates/**/*.md", "nanobot/skills/**/*.md", "nanobot/skills/**/*.sh", + "nanobot/web/dist/**/*", +] +# nanobot/web/dist/ is produced by `cd webui && bun run build` and is +# git-ignored. List it as an artifact so hatch ships it in both wheel and +# sdist even though VCS does not track it. +artifacts = [ + "nanobot/web/dist/**/*", ] [tool.hatch.build.targets.wheel] @@ -141,7 +151,9 @@ packages = ["nanobot"] [tool.hatch.build.targets.sdist] include = [ "nanobot/", + "nanobot/web/dist/", "bridge/", + "hatch_build.py", "README.md", "LICENSE", "THIRD_PARTY_NOTICES.md", diff --git a/webui/README.md b/webui/README.md index b99874ba0..8538bc1ed 100644 --- a/webui/README.md +++ b/webui/README.md @@ -8,15 +8,11 @@ on the same port. For the project overview, install guide, and general docs map, see the root [`README.md`](../README.md). -## Current status +## Just want to use the WebUI? -> [!NOTE] -> The standalone WebUI development workflow currently requires a source -> checkout. -> -> WebUI changes in the GitHub repository may land before they are included in -> the next packaged release, so source installs and published package versions -> are not yet guaranteed to move in lockstep. +If you installed nanobot via `pip install nanobot-ai`, the WebUI is **already bundled** in the wheel. Enable the WebSocket channel in `~/.nanobot/config.json` and run `nanobot gateway` โ€” see the root [`README.md`](../README.md#-webui) for the 3-step setup. You do **not** need anything in this directory. + +This `webui/` tree is for people **hacking on the WebUI itself** (UI changes, new components, styling, etc.). ## Layout @@ -25,7 +21,7 @@ webui/ source tree (this directory) nanobot/web/dist/ build output served by the gateway ``` -## Develop from source +## Develop the WebUI (Vite HMR) ### 1. Install nanobot from source @@ -35,6 +31,8 @@ From the repository root: pip install -e . ``` +> Editable installs intentionally **skip** the WebUI bundle step โ€” Vite HMR is faster than rebuilding `dist/` on every change. + ### 2. Enable the WebSocket channel In `~/.nanobot/config.json`: @@ -63,8 +61,7 @@ bun run dev Then open `http://127.0.0.1:5173`. -By default, the dev server proxies `/api`, `/webui`, `/auth`, and WebSocket -traffic to `http://127.0.0.1:8765`. +By default the dev server proxies `/api`, `/webui`, `/auth`, and WebSocket traffic to `http://127.0.0.1:8765`. If your gateway listens on a non-default port, point the dev server at it: @@ -74,7 +71,7 @@ NANOBOT_API_URL=http://127.0.0.1:9000 bun run dev ### Access from another device (LAN) -To use the webui from another device on the same network, set `host` to `"0.0.0.0"` and configure a `token` or `tokenIssueSecret` in `~/.nanobot/config.json`: +To use the WebUI from another device on the same network, set `host` to `"0.0.0.0"` and configure a `token` or `tokenIssueSecret` in `~/.nanobot/config.json`: ```json { @@ -91,20 +88,20 @@ To use the webui from another device on the same network, set `host` to `"0.0.0. The gateway will refuse to start if `host` is `"0.0.0.0"` and neither `token` nor `tokenIssueSecret` is set. -Then open `http://:8765` on the other device. The webui will show an authentication form where you enter the secret. It is saved in your browser so you only need to enter it once. +Then open `http://:8765` on the other device. The WebUI will show an authentication form where you enter the secret. It is saved in your browser so you only need to enter it once. ## Build for packaged runtime +You usually do not need to run this by hand: `python -m build` invokes the WebUI build automatically when packaging the wheel. + +If you want to preview the production bundle locally without rebuilding the wheel: + ```bash cd webui -bun run build +bun run build # writes to ../nanobot/web/dist ``` -This writes the production assets to `../nanobot/web/dist`, which is the -directory served by `nanobot gateway` and bundled into the Python wheel. - -If you are cutting a release, run the build before packaging so the published -wheel contains the current WebUI assets. +The gateway picks up the new bundle on the next restart. ## Test diff --git a/webui/bun.lock b/webui/bun.lock index e71f2dc54..7f53084c0 100644 --- a/webui/bun.lock +++ b/webui/bun.lock @@ -15,9 +15,11 @@ "@radix-ui/react-tooltip": "^1.1.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "i18next": "^26.0.6", "lucide-react": "^0.469.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^17.0.4", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.6.1", "rehype-katex": "^7.0.1", @@ -506,8 +508,12 @@ "highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="], + "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "i18next": ["i18next@26.2.0", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], @@ -718,6 +724,8 @@ "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + "react-i18next": ["react-i18next@17.0.8", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw=="], + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "react-markdown": ["react-markdown@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw=="], @@ -860,6 +868,8 @@ "vitest": ["vitest@2.1.9", "", { "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", "@vitest/pretty-format": "^2.1.9", "@vitest/runner": "2.1.9", "@vitest/snapshot": "2.1.9", "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "2.1.9", "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="], + "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], diff --git a/webui/src/main.tsx b/webui/src/main.tsx index 75460720f..f385ff61d 100644 --- a/webui/src/main.tsx +++ b/webui/src/main.tsx @@ -1,4 +1,3 @@ -import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; diff --git a/webui/tsconfig.build.json b/webui/tsconfig.build.json index 8b218ef93..756703b27 100644 --- a/webui/tsconfig.build.json +++ b/webui/tsconfig.build.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "types": ["node"] + "types": ["node", "vite/client"] }, "exclude": ["src/tests/**"] }