diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 721a7466070..dc2d1363363 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -5,6 +5,46 @@ nanobot Python distribution (`pip install nanobot-ai`). --- +## Canvas UI Particle Object — WebUI particle background (MIT + Commons Clause) + +- **Source**: https://github.com/DavidHDev/canvas-ui +- **Bundled**: `webui/src/components/canvasui/ParticleObject.tsx` and the compiled + `nanobot/web/dist/assets/ParticleObject-*.js` +- **Modifications**: adapted for nanobot's React WebUI, pointer interaction, reduced-motion + behavior, and mobile scrolling + +``` +MIT + Commons Clause License Condition v1.0 + +Copyright (c) 2026 David Haz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, and distribute the Software as part of +an application, website, or product, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +Commons Clause Restriction + +You may use this Software, including for any commercial purpose, so long as +you do not sell, sublicense, or redistribute the components themselves - +whether alone, in a bundle, or as a ported version. + +No Warranty + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +--- + ## Tabler Icons — interface icons (MIT) - **Source**: https://github.com/tabler/tabler-icons diff --git a/webui/bun.lock b/webui/bun.lock index 0fdd597d9e6..37822aa67ce 100644 --- a/webui/bun.lock +++ b/webui/bun.lock @@ -27,6 +27,7 @@ "remark-math": "^6.0.0", "streamdown": "2.5.0", "tailwind-merge": "^2.6.0", + "three": "^0.185.1", }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -39,6 +40,7 @@ "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/react-syntax-highlighter": "^15.5.13", + "@types/three": "^0.185.1", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "2.1.9", "autoprefixer": "^10.4.20", @@ -112,6 +114,8 @@ "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], + "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], @@ -344,6 +348,8 @@ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -448,10 +454,16 @@ "@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="], + "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], + + "@types/three": ["@types/three@0.185.1", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.2", "meshoptimizer": "~1.1.1" } }, "sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.62.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.62.1", "@typescript-eslint/type-utils": "8.62.1", "@typescript-eslint/utils": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.62.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.62.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.62.1", "@typescript-eslint/types": "8.62.1", "@typescript-eslint/typescript-estree": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA=="], @@ -746,6 +758,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -972,6 +986,8 @@ "mermaid": ["mermaid@11.16.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.20", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "katex": "^0.16.45", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA=="], + "meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], @@ -1254,6 +1270,8 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "three": ["three@0.185.1", "", {}, "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], diff --git a/webui/package-lock.json b/webui/package-lock.json index 48674daa11c..a5654756c54 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -30,7 +30,8 @@ "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "streamdown": "2.5.0", - "tailwind-merge": "^2.6.0" + "tailwind-merge": "^2.6.0", + "three": "^0.185.1" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -43,6 +44,7 @@ "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/react-syntax-highlighter": "^15.5.13", + "@types/three": "^0.185.1", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "2.1.9", "autoprefixer": "^10.4.20", @@ -382,6 +384,13 @@ "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", "license": "Apache-2.0" }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -2456,6 +2465,13 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/aria-query": { "version": "5.0.4", "dev": true, @@ -2862,6 +2878,28 @@ "@types/react": "*" } }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.1.tgz", + "integrity": "sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -2873,6 +2911,13 @@ "version": "3.0.3", "license": "MIT" }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.4", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", @@ -4827,6 +4872,13 @@ } } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6385,6 +6437,13 @@ "node": ">= 20" } }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "dev": true, + "license": "MIT" + }, "node_modules/micromark": { "version": "4.0.2", "funding": [ @@ -8380,6 +8439,12 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "dev": true, diff --git a/webui/package.json b/webui/package.json index c08c5896c33..2d249b5df56 100644 --- a/webui/package.json +++ b/webui/package.json @@ -34,7 +34,8 @@ "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "streamdown": "2.5.0", - "tailwind-merge": "^2.6.0" + "tailwind-merge": "^2.6.0", + "three": "^0.185.1" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -47,6 +48,7 @@ "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/react-syntax-highlighter": "^15.5.13", + "@types/three": "^0.185.1", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "2.1.9", "autoprefixer": "^10.4.20", diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 5fdfddf472c..3107bbf04d3 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -11,6 +11,7 @@ import { import { Eye, EyeOff, Moon, PanelLeft, ShieldCheck, Sun, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { channelUiPresentation } from "@/channel-plugins/registry"; +import { NanobotParticleMark } from "@/components/NanobotParticleMark"; import { Sidebar } from "@/components/Sidebar"; import type { SidebarDeleteItem } from "@/components/ChatList"; import type { SettingsSectionKey } from "@/components/settings/SettingsView"; @@ -336,6 +337,7 @@ function AuthForm({ onSecret: (secret: string) => void; }) { const { t } = useTranslation(); + const { theme } = useTheme(); const inputRef = useRef(null); const [value, setValue] = useState(""); const [passwordVisible, setPasswordVisible] = useState(false); @@ -358,67 +360,86 @@ function AuthForm({ }; return ( -
-
+
-
-

- +
+ +

+ nanobot

-
- { - setValue(e.target.value); - setValidationError(null); - }} - disabled={submitting} - aria-invalid={validationError ? true : undefined} - aria-describedby={validationError ? "webui-auth-error" : undefined} - className="pr-10" - autoFocus - /> -
+ +
+ +
+ { + setValue(e.target.value); + setValidationError(null); + }} + disabled={submitting} + aria-invalid={validationError ? true : undefined} + aria-describedby={validationError ? "webui-auth-error" : undefined} + className="auth-password-input h-11 rounded-lg bg-background pr-11 text-base shadow-none" + autoFocus + /> + +
+ {errorMessage ? ( + + ) : null}
- {errorMessage ? ( - - ) : null} -

- - -
+ + + + ); } diff --git a/webui/src/components/NanobotParticleMark.tsx b/webui/src/components/NanobotParticleMark.tsx new file mode 100644 index 00000000000..3c5307d4296 --- /dev/null +++ b/webui/src/components/NanobotParticleMark.tsx @@ -0,0 +1,65 @@ +import { lazy, Suspense } from "react"; + +import { useMediaQuery } from "@/hooks/useMediaQuery"; + +interface NanobotParticleMarkProps { + theme: "light" | "dark"; +} + +const LOGO_SRC = "/brand/nanobot_mark.svg"; +const PARTICLES_ENABLED = import.meta.env.MODE !== "test"; + +const ParticleObject = lazy(async () => { + const module = await import("@/components/canvasui/ParticleObject"); + return { default: module.ParticleObject }; +}); + +export function NanobotParticleMark({ theme }: NanobotParticleMarkProps) { + const compact = useMediaQuery("(max-width: 639px)"); + + return ( + ); + const emptyStateBackground = loading ? null : ( + + ); const sessionInfoAction = historyKey ? ( ) : undefined; @@ -1566,6 +1570,7 @@ export function ThreadShell({ temporary={temporary} isStreaming={turnActive} emptyState={emptyState} + emptyStateBackground={emptyStateBackground} composer={composerPortalTarget === undefined ? composer : null} activeTurnId={viewportTurnId} activeTurnStartedHere={activeTurnStartedHere} diff --git a/webui/src/components/thread/ThreadViewport.tsx b/webui/src/components/thread/ThreadViewport.tsx index 919396dec21..ca258502ae0 100644 --- a/webui/src/components/thread/ThreadViewport.tsx +++ b/webui/src/components/thread/ThreadViewport.tsx @@ -39,6 +39,7 @@ interface ThreadViewportProps { isStreaming: boolean; composer?: ReactNode; emptyState?: ReactNode; + emptyStateBackground?: ReactNode; scrollToBottomSignal?: number; activeTurnId?: string | null; activeTurnStartedHere?: boolean; @@ -163,6 +164,7 @@ export const ThreadViewport = forwardRef + {!hasMessages && emptyStateBackground ? ( +
+ {emptyStateBackground} +
+ ) : null} {hasMessages ? (
@@ -737,8 +744,10 @@ export const ThreadViewport = forwardRef
) : null}
diff --git a/webui/src/globals.css b/webui/src/globals.css index c67e05d2b90..57bbf865f61 100644 --- a/webui/src/globals.css +++ b/webui/src/globals.css @@ -724,6 +724,19 @@ container-type: inline-size; } +.auth-password-input:-webkit-autofill, +.auth-password-input:-webkit-autofill:hover, +.auth-password-input:-webkit-autofill:focus { + -webkit-text-fill-color: hsl(var(--foreground)); + caret-color: hsl(var(--foreground)); + box-shadow: inset 0 0 0 1000px hsl(var(--background)); +} + +.auth-password-input:-webkit-autofill:focus-visible { + outline: 2px solid hsl(var(--ring) / 0.5); + outline-offset: -2px; +} + .thread-composer-footer-actions { flex: 0 1 auto; max-width: 58%; diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx index cc192b7b83a..2df5c069abf 100644 --- a/webui/src/tests/app-layout.test.tsx +++ b/webui/src/tests/app-layout.test.tsx @@ -346,7 +346,7 @@ describe("App layout", () => { render(); - expect(await screen.findByRole("heading", { level: 1, name: "Password" })) + expect(await screen.findByRole("heading", { level: 1, name: "nanobot" })) .toBeInTheDocument(); const password = screen.getByLabelText("Password"); expect(password).toHaveAttribute( @@ -354,6 +354,11 @@ describe("App layout", () => { "current-password", ); expect(password).not.toHaveAttribute("placeholder"); + expect(password).toHaveClass("bg-background"); + expect(screen.getByTestId("nanobot-particle-mark")).toHaveAttribute( + "aria-hidden", + "true", + ); expect(screen.queryByText("Authentication required")).not.toBeInTheDocument(); expect( screen.queryByText("Incorrect password. Try again."), @@ -417,7 +422,7 @@ describe("App layout", () => { render(); - expect(await screen.findByRole("heading", { level: 1, name: "Password" })) + expect(await screen.findByRole("heading", { level: 1, name: "nanobot" })) .toBeInTheDocument(); expect( screen.queryByText("Incorrect password. Try again."), diff --git a/webui/src/tests/nanobot-particle-mark.test.tsx b/webui/src/tests/nanobot-particle-mark.test.tsx new file mode 100644 index 00000000000..86395fca7b1 --- /dev/null +++ b/webui/src/tests/nanobot-particle-mark.test.tsx @@ -0,0 +1,56 @@ +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; + +import { render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { NanobotParticleMark } from "@/components/NanobotParticleMark"; + +function stubViewport(compact: boolean) { + vi.stubGlobal( + "matchMedia", + vi.fn((query: string) => ({ + matches: compact && query === "(max-width: 639px)", + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + ); +} + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("NanobotParticleMark", () => { + it("uses the desktop particle profile on wider viewports", () => { + stubViewport(false); + + render(); + + const mark = screen.getByTestId("nanobot-particle-mark"); + expect(mark).toHaveAttribute( + "data-particle-profile", + "desktop", + ); + expect(mark.querySelector("img")).toBeNull(); + expect( + existsSync(resolve(process.cwd(), "public", "brand", "nanobot_mark.svg")), + ).toBe(true); + }); + + it("uses the compact particle profile on mobile viewports", () => { + stubViewport(true); + + render(); + + expect(screen.getByTestId("nanobot-particle-mark")).toHaveAttribute( + "data-particle-profile", + "compact", + ); + }); +}); diff --git a/webui/src/tests/thread-composer.test.tsx b/webui/src/tests/thread-composer.test.tsx index 4fbab537ef2..1c9c237b04d 100644 --- a/webui/src/tests/thread-composer.test.tsx +++ b/webui/src/tests/thread-composer.test.tsx @@ -476,7 +476,11 @@ describe("ThreadComposer", () => { expect(input.className).toContain("pt-[27px]"); fireEvent.change(input, { target: { value: "1" } }); expect(input.className).toContain("pt-[27px]"); - expect(input.parentElement?.parentElement?.className).toContain("max-w-[58rem]"); + const surface = input.parentElement?.parentElement; + expect(surface?.className).toContain("max-w-[58rem]"); + expect(surface?.className).toContain("bg-card"); + expect(surface?.className).not.toContain("bg-muted/30"); + expect(surface?.className).not.toContain("shadow-"); }); it("defers textarea autosizing until IME composition commits", () => { diff --git a/webui/src/tests/thread-shell.test.tsx b/webui/src/tests/thread-shell.test.tsx index e17884ce1bd..17040d59188 100644 --- a/webui/src/tests/thread-shell.test.tsx +++ b/webui/src/tests/thread-shell.test.tsx @@ -1272,6 +1272,7 @@ describe("ThreadShell", () => { const greeting = screen.getByRole("heading", { level: 1, name: HERO_GREETING_PATTERN }); expect(greeting).toHaveAttribute("data-testid", "hero-greeting"); expect(greeting).toHaveClass("select-none", "whitespace-nowrap"); + expect(screen.getByTestId("nanobot-particle-mark")).toHaveAttribute("aria-hidden", "true"); expect(screen.getByPlaceholderText("Ask anything...")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Write code" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Create a project plan" })).not.toBeInTheDocument(); diff --git a/webui/src/tests/thread-viewport.test.tsx b/webui/src/tests/thread-viewport.test.tsx index 2e2f7c71fdb..978ad07aec9 100644 --- a/webui/src/tests/thread-viewport.test.tsx +++ b/webui/src/tests/thread-viewport.test.tsx @@ -1290,18 +1290,25 @@ describe("ThreadViewport", () => { isStreaming={false} composer={
composer
} emptyState={
welcome
} + emptyStateBackground={
} />, ); const layout = screen.getByTestId("thread-welcome-layout"); expect(layout).toHaveClass("thread-layout", "grid"); expect(layout).toHaveAttribute("data-layout", "hero"); + expect(screen.getByTestId("welcome-background").parentElement).toHaveClass( + "z-20", + ); expect(screen.getByText("welcome").parentElement).toHaveClass( "min-h-0", "items-center", + "pointer-events-none", "sm:items-end", "sm:pb-8", + "z-30", ); + expect(screen.getByTestId("thread-composer-dock")).toHaveClass("z-30"); expect(screen.getByTestId("thread-composer-motion")).toContainElement( screen.getByText("composer"), );