diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..3323f5464c --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,8 @@ +# actionlint configuration +# https://github.com/rhysd/actionlint/blob/main/docs/config.md + +paths: + .github/workflows/release.yml: + ignore: + # Optional signing secrets — linter cannot verify repo secrets exist + - "Context access might be invalid:.+" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 851d89a897..a455f5f96c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -163,19 +163,19 @@ jobs: [ -z "$CSC_LINK" ] && unset CSC_LINK CSC_KEY_PASSWORD [ -z "$APPLE_ID" ] && unset APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID [ -z "$AZURE_TENANT_ID" ] && unset AZURE_TENANT_ID AZURE_CLIENT_ID AZURE_CLIENT_SECRET - bunx electron-builder build ${{ matrix.platform.electron-args }} -c ./electron-builder.config.json -p never + npx electron-builder build ${{ matrix.platform.electron-args }} -c ./electron-builder.config.json -p never working-directory: apps/app/electron shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + CSC_LINK: ${{ secrets.CSC_LINK || '' }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD || '' }} + APPLE_ID: ${{ secrets.APPLE_ID || '' }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID || '' }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID || '' }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID || '' }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET || '' }} # If signing failed, retry without signing so we still get an unsigned artifact - name: Package desktop app (unsigned fallback) @@ -193,7 +193,7 @@ jobs: " unset CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID unset AZURE_TENANT_ID AZURE_CLIENT_ID AZURE_CLIENT_SECRET - CSC_IDENTITY_AUTO_DISCOVERY=false bunx electron-builder build ${{ matrix.platform.electron-args }} -c ./eb-unsigned.json -p never + CSC_IDENTITY_AUTO_DISCOVERY=false npx electron-builder build ${{ matrix.platform.electron-args }} -c ./eb-unsigned.json -p never working-directory: apps/app/electron shell: bash env: diff --git a/.gitignore b/.gitignore index 5475a3bd65..9c49ba7fef 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,9 @@ skills/ apps/app/electron/.eliza apps/app/electron/skills/ +# Temporary capture files +temp_capture_*.jpg + # Agent learning docs & knowledge base knowledge/ scripts/knowledge-loop.mjs \ No newline at end of file diff --git a/apps/app/electron/package.json b/apps/app/electron/package.json index c33e1a9c99..3c1dca1871 100644 --- a/apps/app/electron/package.json +++ b/apps/app/electron/package.json @@ -33,8 +33,8 @@ "whisper-node": "^1.1.1" }, "devDependencies": { - "electron": "^26.6.10", - "electron-builder": "^25.1.8", + "electron": "26.6.10", + "electron-builder": "^26.7.0", "electron-rebuild": "^3.2.9", "typescript": "^5.0.4" }, diff --git a/apps/app/electron/src/setup.ts b/apps/app/electron/src/setup.ts index 6e07b0c489..3dabefdb90 100644 --- a/apps/app/electron/src/setup.ts +++ b/apps/app/electron/src/setup.ts @@ -278,7 +278,7 @@ export function setupContentSecurityPolicy(customScheme: string): void { `default-src 'self' ${customScheme}://* https://*`, `script-src 'self' ${customScheme}://* 'unsafe-inline'${electronIsDev ? " 'unsafe-eval' devtools://*" : ''}`, `style-src 'self' ${customScheme}://* 'unsafe-inline'`, - `connect-src 'self' ${customScheme}://* http://localhost:* ws://localhost:* wss://localhost:* https://* wss://*`, + `connect-src 'self' ${customScheme}://* blob: http://localhost:* ws://localhost:* wss://localhost:* https://* wss://*`, `img-src 'self' ${customScheme}://* data: blob: https://*`, `media-src 'self' ${customScheme}://* blob: https://*`, `font-src 'self' ${customScheme}://* data:`, diff --git a/apps/app/index.html b/apps/app/index.html index 25f15841ec..a51726184c 100644 --- a/apps/app/index.html +++ b/apps/app/index.html @@ -12,7 +12,7 @@ content="default-src 'self' capacitor-electron://* https://*; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; - connect-src 'self' http://localhost:* ws://localhost:* wss://localhost:* https://* wss://*; + connect-src 'self' blob: http://localhost:* ws://localhost:* wss://localhost:* https://* wss://*; img-src 'self' data: blob: https://*; media-src 'self' blob: https://*; font-src 'self' data:;" /> diff --git a/apps/app/package-lock.json b/apps/app/package-lock.json index 2455452566..36f21ec3ab 100644 --- a/apps/app/package-lock.json +++ b/apps/app/package-lock.json @@ -27,14 +27,20 @@ "@milaidy/capacitor-talkmode": "file:plugins/talkmode", "@noble/ed25519": "3.0.0", "dompurify": "^3.3.1", - "lit": "^3.3.2", - "marked": "^17.0.1" + "marked": "^17.0.1", + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "devDependencies": { "@capacitor-community/electron": "^5.0.1", "@capacitor/cli": "8.0.2", + "@tailwindcss/vite": "^4.1.18", "@types/dompurify": "^3.0.5", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^5.1.3", "esbuild": "^0.27.3", + "tailwindcss": "^4.1.18", "typescript": "^5.9.3", "vite": "^5.4.2", "vitest": "^2.0.0" @@ -51,13 +57,238 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", - "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, "engines": { "node": ">=6.9.0" } @@ -164,6 +395,132 @@ "node": ">=4" } }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@capacitor-community/electron": { "version": "5.0.1", "dev": true, @@ -1565,20 +1922,52 @@ "node": ">=18.0.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "dev": true, "license": "MIT" }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.5.1", - "license": "BSD-3-Clause" - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.2", - "license": "BSD-3-Clause", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@milaidy/capacitor-agent": { @@ -1673,6 +2062,13 @@ "node": ">=14" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-node-resolve": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", @@ -2083,6 +2479,323 @@ "dev": true, "license": "MIT" }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/dompurify": { "version": "3.2.0", "deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.", @@ -2127,6 +2840,26 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/react": { + "version": "19.2.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", + "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -2141,7 +2874,8 @@ }, "node_modules/@types/trusted-types": { "version": "2.0.7", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "4.33.0", @@ -2314,6 +3048,27 @@ "dev": true, "license": "ISC" }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.3.tgz", + "integrity": "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.2", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/@vitest/expect": { "version": "2.1.9", "dev": true, @@ -2712,6 +3467,16 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/big-integer": { "version": "1.6.52", "dev": true, @@ -2762,6 +3527,40 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer": { "version": "5.7.1", "dev": true, @@ -2861,6 +3660,27 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "5.3.3", "dev": true, @@ -2975,6 +3795,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "dev": true, @@ -2988,6 +3815,13 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3138,6 +3972,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3201,6 +4045,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, "node_modules/elementtree": { "version": "0.1.7", "dev": true, @@ -3222,6 +4073,20 @@ "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -3441,6 +4306,16 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4274,6 +5149,16 @@ "node": ">= 0.4" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5133,13 +6018,22 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/js-yaml": { "version": "3.14.2", @@ -5156,6 +6050,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "dev": true, @@ -5229,29 +6136,265 @@ "node": ">= 0.8.0" } }, - "node_modules/lit": { - "version": "3.3.2", - "license": "BSD-3-Clause", + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lit-element": { - "version": "4.2.2", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lit-html": { - "version": "3.3.2", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/locate-path": { @@ -5512,6 +6655,13 @@ "dev": true, "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5991,6 +7141,37 @@ ], "license": "MIT" }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "dev": true, @@ -6293,6 +7474,12 @@ "node": ">=11.0.0" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.3", "dev": true, @@ -6749,6 +7936,27 @@ "license": "MIT", "peer": true }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar": { "version": "7.5.7", "dev": true, @@ -7030,6 +8238,37 @@ "node": ">=8" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/apps/app/package.json b/apps/app/package.json index f9528216ff..7d94a5fad4 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -33,6 +33,7 @@ "@capacitor/keyboard": "8.0.0", "@capacitor/preferences": "8.0.0", "@capacitor/status-bar": "8.0.0", + "@milaidy/capacitor-agent": "file:plugins/agent", "@milaidy/capacitor-camera": "file:plugins/camera", "@milaidy/capacitor-canvas": "file:plugins/canvas", "@milaidy/capacitor-desktop": "file:plugins/desktop", @@ -40,23 +41,19 @@ "@milaidy/capacitor-location": "file:plugins/location", "@milaidy/capacitor-screencapture": "file:plugins/screencapture", "@milaidy/capacitor-swabble": "file:plugins/swabble", - "@milaidy/capacitor-agent": "file:plugins/agent", "@milaidy/capacitor-talkmode": "file:plugins/talkmode", - "@noble/ed25519": "3.0.0", - "dompurify": "^3.3.1", - "marked": "^17.0.1", + "@pixiv/three-vrm": "^3.4.5", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "three": "^0.182.0" }, "devDependencies": { - "@capacitor-community/electron": "^5.0.1", "@capacitor/cli": "8.0.2", - "@types/dompurify": "^3.0.5", "@tailwindcss/vite": "^4.1.18", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@types/three": "^0.182.0", "@vitejs/plugin-react": "^5.1.3", - "esbuild": "^0.27.3", "tailwindcss": "^4.1.18", "typescript": "^5.9.3", "vite": "^5.4.2", diff --git a/apps/app/public/animations/Idle.fbx b/apps/app/public/animations/Idle.fbx new file mode 100644 index 0000000000..5a5a107432 Binary files /dev/null and b/apps/app/public/animations/Idle.fbx differ diff --git a/apps/app/public/animations/Standing Greeting.fbx b/apps/app/public/animations/Standing Greeting.fbx new file mode 100644 index 0000000000..3db5cb1ca0 Binary files /dev/null and b/apps/app/public/animations/Standing Greeting.fbx differ diff --git a/apps/app/public/animations/idle.glb b/apps/app/public/animations/idle.glb new file mode 100644 index 0000000000..e1b00c6347 Binary files /dev/null and b/apps/app/public/animations/idle.glb differ diff --git a/apps/app/public/logos/anthropic-icon-white.png b/apps/app/public/logos/anthropic-icon-white.png new file mode 100644 index 0000000000..a4760d1665 Binary files /dev/null and b/apps/app/public/logos/anthropic-icon-white.png differ diff --git a/apps/app/public/logos/anthropic-icon.png b/apps/app/public/logos/anthropic-icon.png new file mode 100644 index 0000000000..ea30ab63a5 Binary files /dev/null and b/apps/app/public/logos/anthropic-icon.png differ diff --git a/apps/app/public/logos/claude-icon.png b/apps/app/public/logos/claude-icon.png new file mode 100644 index 0000000000..3bb97a9293 Binary files /dev/null and b/apps/app/public/logos/claude-icon.png differ diff --git a/apps/app/public/logos/deepseek-icon.png b/apps/app/public/logos/deepseek-icon.png new file mode 100644 index 0000000000..50f74f7b0d Binary files /dev/null and b/apps/app/public/logos/deepseek-icon.png differ diff --git a/apps/app/public/logos/elizaos-icon.png b/apps/app/public/logos/elizaos-icon.png new file mode 100644 index 0000000000..f4939a99fb Binary files /dev/null and b/apps/app/public/logos/elizaos-icon.png differ diff --git a/apps/app/public/logos/gemini-icon.png b/apps/app/public/logos/gemini-icon.png new file mode 100644 index 0000000000..9c186fa08a Binary files /dev/null and b/apps/app/public/logos/gemini-icon.png differ diff --git a/apps/app/public/logos/grok-icon-white.png b/apps/app/public/logos/grok-icon-white.png new file mode 100644 index 0000000000..e1d798668e Binary files /dev/null and b/apps/app/public/logos/grok-icon-white.png differ diff --git a/apps/app/public/logos/grok-icon.png b/apps/app/public/logos/grok-icon.png new file mode 100644 index 0000000000..ac83021513 Binary files /dev/null and b/apps/app/public/logos/grok-icon.png differ diff --git a/apps/app/public/logos/groq-icon-white.png b/apps/app/public/logos/groq-icon-white.png new file mode 100644 index 0000000000..b82486039b Binary files /dev/null and b/apps/app/public/logos/groq-icon-white.png differ diff --git a/apps/app/public/logos/groq-icon.png b/apps/app/public/logos/groq-icon.png new file mode 100644 index 0000000000..12180dab1d Binary files /dev/null and b/apps/app/public/logos/groq-icon.png differ diff --git a/apps/app/public/logos/mistral-icon.png b/apps/app/public/logos/mistral-icon.png new file mode 100644 index 0000000000..f6810142f7 Binary files /dev/null and b/apps/app/public/logos/mistral-icon.png differ diff --git a/apps/app/public/logos/ollama-icon-white.png b/apps/app/public/logos/ollama-icon-white.png new file mode 100644 index 0000000000..873fe7c02c Binary files /dev/null and b/apps/app/public/logos/ollama-icon-white.png differ diff --git a/apps/app/public/logos/ollama-icon.png b/apps/app/public/logos/ollama-icon.png new file mode 100644 index 0000000000..0299134546 Binary files /dev/null and b/apps/app/public/logos/ollama-icon.png differ diff --git a/apps/app/public/logos/openai-icon-white.png b/apps/app/public/logos/openai-icon-white.png new file mode 100644 index 0000000000..44c0560c30 Binary files /dev/null and b/apps/app/public/logos/openai-icon-white.png differ diff --git a/apps/app/public/logos/openai-icon.png b/apps/app/public/logos/openai-icon.png new file mode 100644 index 0000000000..147e939a46 Binary files /dev/null and b/apps/app/public/logos/openai-icon.png differ diff --git a/apps/app/public/logos/openrouter-icon-white.png b/apps/app/public/logos/openrouter-icon-white.png new file mode 100644 index 0000000000..8118d79fa3 Binary files /dev/null and b/apps/app/public/logos/openrouter-icon-white.png differ diff --git a/apps/app/public/logos/openrouter-icon.png b/apps/app/public/logos/openrouter-icon.png new file mode 100644 index 0000000000..12ea4bcf61 Binary files /dev/null and b/apps/app/public/logos/openrouter-icon.png differ diff --git a/apps/app/public/logos/together-ai-icon.png b/apps/app/public/logos/together-ai-icon.png new file mode 100644 index 0000000000..af4e63ecfc Binary files /dev/null and b/apps/app/public/logos/together-ai-icon.png differ diff --git a/apps/app/public/logos/zai-icon-white.png b/apps/app/public/logos/zai-icon-white.png new file mode 100644 index 0000000000..b4e730fadf Binary files /dev/null and b/apps/app/public/logos/zai-icon-white.png differ diff --git a/apps/app/public/logos/zai-icon.png b/apps/app/public/logos/zai-icon.png new file mode 100644 index 0000000000..8cfe5351cd Binary files /dev/null and b/apps/app/public/logos/zai-icon.png differ diff --git a/apps/app/public/screenshotter.html b/apps/app/public/screenshotter.html new file mode 100644 index 0000000000..994cdfaf17 --- /dev/null +++ b/apps/app/public/screenshotter.html @@ -0,0 +1,399 @@ + + + + +VRM Screenshotter + + + +

VRM Screenshotter

+

Renders each VRM in idle pose and captures a framed preview image.

+
+ + +
+
+ + + + diff --git a/apps/app/public/vrms/1.vrm b/apps/app/public/vrms/1.vrm new file mode 100644 index 0000000000..41d4915df9 Binary files /dev/null and b/apps/app/public/vrms/1.vrm differ diff --git a/apps/app/public/vrms/2.vrm b/apps/app/public/vrms/2.vrm new file mode 100644 index 0000000000..2af0a87baf Binary files /dev/null and b/apps/app/public/vrms/2.vrm differ diff --git a/apps/app/public/vrms/3.vrm b/apps/app/public/vrms/3.vrm new file mode 100644 index 0000000000..ced900e0d0 Binary files /dev/null and b/apps/app/public/vrms/3.vrm differ diff --git a/apps/app/public/vrms/4.vrm b/apps/app/public/vrms/4.vrm new file mode 100644 index 0000000000..936b46687e Binary files /dev/null and b/apps/app/public/vrms/4.vrm differ diff --git a/apps/app/public/vrms/5.vrm b/apps/app/public/vrms/5.vrm new file mode 100644 index 0000000000..98f9972312 Binary files /dev/null and b/apps/app/public/vrms/5.vrm differ diff --git a/apps/app/public/vrms/6.vrm b/apps/app/public/vrms/6.vrm new file mode 100644 index 0000000000..6290378f26 Binary files /dev/null and b/apps/app/public/vrms/6.vrm differ diff --git a/apps/app/public/vrms/7.vrm b/apps/app/public/vrms/7.vrm new file mode 100644 index 0000000000..d624e449ef Binary files /dev/null and b/apps/app/public/vrms/7.vrm differ diff --git a/apps/app/public/vrms/8.vrm b/apps/app/public/vrms/8.vrm new file mode 100644 index 0000000000..d80d4d6293 Binary files /dev/null and b/apps/app/public/vrms/8.vrm differ diff --git a/apps/app/public/vrms/previews/milady-1.png b/apps/app/public/vrms/previews/milady-1.png new file mode 100644 index 0000000000..d079f964c3 Binary files /dev/null and b/apps/app/public/vrms/previews/milady-1.png differ diff --git a/apps/app/public/vrms/previews/milady-2.png b/apps/app/public/vrms/previews/milady-2.png new file mode 100644 index 0000000000..57058180b6 Binary files /dev/null and b/apps/app/public/vrms/previews/milady-2.png differ diff --git a/apps/app/public/vrms/previews/milady-3.png b/apps/app/public/vrms/previews/milady-3.png new file mode 100644 index 0000000000..92b509e3fb Binary files /dev/null and b/apps/app/public/vrms/previews/milady-3.png differ diff --git a/apps/app/public/vrms/previews/milady-4.png b/apps/app/public/vrms/previews/milady-4.png new file mode 100644 index 0000000000..9fa215b8c4 Binary files /dev/null and b/apps/app/public/vrms/previews/milady-4.png differ diff --git a/apps/app/public/vrms/previews/milady-5.png b/apps/app/public/vrms/previews/milady-5.png new file mode 100644 index 0000000000..ffd5aa31a7 Binary files /dev/null and b/apps/app/public/vrms/previews/milady-5.png differ diff --git a/apps/app/public/vrms/previews/milady-6.png b/apps/app/public/vrms/previews/milady-6.png new file mode 100644 index 0000000000..121903f32c Binary files /dev/null and b/apps/app/public/vrms/previews/milady-6.png differ diff --git a/apps/app/public/vrms/previews/milady-7.png b/apps/app/public/vrms/previews/milady-7.png new file mode 100644 index 0000000000..146bcc3ce8 Binary files /dev/null and b/apps/app/public/vrms/previews/milady-7.png differ diff --git a/apps/app/public/vrms/previews/milady-8.png b/apps/app/public/vrms/previews/milady-8.png new file mode 100644 index 0000000000..89593ab35e Binary files /dev/null and b/apps/app/public/vrms/previews/milady-8.png differ diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index 46f9be9f6d..95bc11446d 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -11,14 +11,15 @@ import { OnboardingWizard } from "./components/OnboardingWizard.js"; import { ChatView } from "./components/ChatView.js"; import { ConversationsSidebar } from "./components/ConversationsSidebar.js"; import { WidgetSidebar } from "./components/WidgetSidebar.js"; -import { PluginsView } from "./components/PluginsView.js"; +import { FeaturesView, ConnectorsView } from "./components/PluginsView.js"; import { SkillsView } from "./components/SkillsView.js"; import { InventoryView } from "./components/InventoryView.js"; +import { CharacterView } from "./components/CharacterView.js"; import { ConfigView } from "./components/ConfigView.js"; -import { LogsView } from "./components/LogsView.js"; +import { AdminView } from "./components/AdminView.js"; import { AppsView } from "./components/AppsView.js"; import { GameView } from "./components/GameView.js"; -import { DatabaseView } from "./components/DatabaseView.js"; +import { LoadingScreen } from "./components/LoadingScreen.js"; function ViewRouter() { const { tab } = useApp(); @@ -27,11 +28,12 @@ function ViewRouter() { case "apps": return ; case "game": return ; case "inventory": return ; - case "plugins": return ; + case "features": return ; + case "connectors": return ; case "skills": return ; - case "database": return ; + case "character": return ; case "config": return ; - case "logs": return ; + case "admin": return ; default: return ; } } @@ -40,11 +42,7 @@ export function App() { const { onboardingLoading, authRequired, onboardingComplete, tab, actionNotice } = useApp(); if (onboardingLoading) { - return ( -
-
Loading...
-
- ); + return ; } if (authRequired) return ; diff --git a/apps/app/src/AppContext.tsx b/apps/app/src/AppContext.tsx index a13ca57a33..3dee6fbf81 100644 --- a/apps/app/src/AppContext.tsx +++ b/apps/app/src/AppContext.tsx @@ -44,6 +44,21 @@ import { import { tabFromPath, pathForTab, type Tab } from "./navigation"; import { SkillScanReportSummary } from "./api-client"; +// ── VRM helpers ───────────────────────────────────────────────────────── + +/** Number of built-in milady VRM avatars shipped with the app. */ +export const VRM_COUNT = 8; + +/** Resolve a built-in VRM index (1–8) to its public asset URL. */ +export function getVrmUrl(index: number): string { + return `/vrms/${index}.vrm`; +} + +/** Resolve a built-in VRM index (1–8) to its preview thumbnail URL. */ +export function getVrmPreviewUrl(index: number): string { + return `/vrms/previews/milady-${index}.png`; +} + // ── Theme ────────────────────────────────────────────────────────────── const THEME_STORAGE_KEY = "milaidy:theme"; @@ -95,6 +110,7 @@ function applyTheme(name: ThemeName) { export type OnboardingStep = | "welcome" | "name" + | "avatar" | "style" | "theme" | "runMode" @@ -102,7 +118,8 @@ export type OnboardingStep = | "modelSelection" | "cloudLogin" | "llmProvider" - | "inventorySetup"; + | "inventorySetup" + | "connectors"; // ── Action notice ────────────────────────────────────────────────────── @@ -195,6 +212,8 @@ export interface AppState { characterSaveSuccess: string | null; characterSaveError: string | null; characterDraft: CharacterData; + selectedVrmIndex: number; + customVrmUrl: string; // Cloud cloudEnabled: boolean; @@ -271,9 +290,21 @@ export interface AppState { onboardingLargeModel: string; onboardingProvider: string; onboardingApiKey: string; + onboardingOpenRouterModel: string; + onboardingTelegramToken: string; + onboardingDiscordToken: string; + onboardingWhatsAppSessionPath: string; + onboardingTwilioAccountSid: string; + onboardingTwilioAuthToken: string; + onboardingTwilioPhoneNumber: string; + onboardingBlooioApiKey: string; + onboardingBlooioPhoneNumber: string; + onboardingSubscriptionTab: "token" | "oauth"; onboardingSelectedChains: Set; onboardingRpcSelections: Record; onboardingRpcKeys: Record; + onboardingAvatar: number; + onboardingRestarting: boolean; // Command palette commandPaletteOpen: boolean; @@ -303,9 +334,6 @@ export interface AppState { activeGameSandbox: string; activeGamePostMessageAuth: boolean; - // Config text - configRaw: Record; - configText: string; } export interface AppActions { @@ -498,6 +526,8 @@ export function AppProvider({ children }: { children: ReactNode }) { const [characterSaveSuccess, setCharacterSaveSuccess] = useState(null); const [characterSaveError, setCharacterSaveError] = useState(null); const [characterDraft, setCharacterDraft] = useState({}); + const [selectedVrmIndex, setSelectedVrmIndex] = useState(1); + const [customVrmUrl, setCustomVrmUrl] = useState(""); // --- Cloud --- const [cloudEnabled, setCloudEnabled] = useState(false); @@ -567,16 +597,28 @@ export function AppProvider({ children }: { children: ReactNode }) { const [onboardingOptions, setOnboardingOptions] = useState(null); const [onboardingName, setOnboardingName] = useState(""); const [onboardingStyle, setOnboardingStyle] = useState(""); - const [onboardingTheme, setOnboardingTheme] = useState("milady"); + const [onboardingTheme, setOnboardingTheme] = useState(loadTheme); const [onboardingRunMode, setOnboardingRunMode] = useState<"local" | "cloud" | "">(""); const [onboardingCloudProvider, setOnboardingCloudProvider] = useState(""); - const [onboardingSmallModel, setOnboardingSmallModel] = useState("claude-haiku"); - const [onboardingLargeModel, setOnboardingLargeModel] = useState("claude-sonnet-4-5"); + const [onboardingSmallModel, setOnboardingSmallModel] = useState("moonshotai/kimi-k2-turbo"); + const [onboardingLargeModel, setOnboardingLargeModel] = useState("moonshotai/kimi-k2-0905"); const [onboardingProvider, setOnboardingProvider] = useState(""); const [onboardingApiKey, setOnboardingApiKey] = useState(""); + const [onboardingOpenRouterModel, setOnboardingOpenRouterModel] = useState(""); + const [onboardingTelegramToken, setOnboardingTelegramToken] = useState(""); + const [onboardingDiscordToken, setOnboardingDiscordToken] = useState(""); + const [onboardingWhatsAppSessionPath, setOnboardingWhatsAppSessionPath] = useState(""); + const [onboardingTwilioAccountSid, setOnboardingTwilioAccountSid] = useState(""); + const [onboardingTwilioAuthToken, setOnboardingTwilioAuthToken] = useState(""); + const [onboardingTwilioPhoneNumber, setOnboardingTwilioPhoneNumber] = useState(""); + const [onboardingBlooioApiKey, setOnboardingBlooioApiKey] = useState(""); + const [onboardingBlooioPhoneNumber, setOnboardingBlooioPhoneNumber] = useState(""); + const [onboardingSubscriptionTab, setOnboardingSubscriptionTab] = useState<"token" | "oauth">("token"); const [onboardingSelectedChains, setOnboardingSelectedChains] = useState>(new Set(["evm", "solana"])); const [onboardingRpcSelections, setOnboardingRpcSelections] = useState>({}); const [onboardingRpcKeys, setOnboardingRpcKeys] = useState>({}); + const [onboardingAvatar, setOnboardingAvatar] = useState(1); + const [onboardingRestarting, setOnboardingRestarting] = useState(false); // --- Command palette --- const [commandPaletteOpen, setCommandPaletteOpen] = useState(false); @@ -606,14 +648,14 @@ export function AppProvider({ children }: { children: ReactNode }) { const [activeGameSandbox, setActiveGameSandbox] = useState("allow-scripts allow-same-origin allow-popups"); const [activeGamePostMessageAuth, setActiveGamePostMessageAuth] = useState(false); - // --- Config --- - const [configRaw, setConfigRaw] = useState>({}); - const [configText, setConfigText] = useState(""); // --- Refs for timers --- const actionNoticeTimer = useRef(null); const cloudPollInterval = useRef(null); const cloudLoginPollTimer = useRef(null); + const prevAgentStateRef = useRef(null); + /** Guards against double-greeting when both init and state-transition paths fire. */ + const greetingFiredRef = useRef(false); // ── Action notice ────────────────────────────────────────────────── @@ -730,7 +772,13 @@ export function AppProvider({ children }: { children: ReactNode }) { try { const { messages } = await client.getConversationMessages(convId); setConversationMessages(messages); - } catch { + } catch (err) { + // If the conversation no longer exists (server restarted), clear it + const status = (err as { status?: number }).status; + if (status === 404) { + setActiveConversationId(null); + setConversations([]); + } setConversationMessages([]); } }, []); @@ -843,9 +891,17 @@ export function AppProvider({ children }: { children: ReactNode }) { const cloudStatus = await client.getCloudStatus().catch(() => null); if (!cloudStatus) return; setCloudEnabled(cloudStatus.enabled ?? false); - setCloudConnected(cloudStatus.connected); + // Treat as connected if EITHER the backend says connected OR the config + // has the API key saved (hasApiKey). The CLOUD_AUTH service may not have + // refreshed yet, but the key is persisted and model calls will work. + const isConnected = cloudStatus.connected || (cloudStatus.enabled && cloudStatus.hasApiKey); + setCloudConnected(Boolean(isConnected)); setCloudUserId(cloudStatus.userId ?? null); if (cloudStatus.topUpUrl) setCloudTopUpUrl(cloudStatus.topUpUrl); + // Only fetch credits when the backend confirms the auth service is + // actually connected. When hasApiKey is true but connected is false, + // the CLOUD_AUTH service hasn't authenticated yet -- fetching credits + // would just fail and spam warnings in the backend logs. if (cloudStatus.connected) { const credits = await client.getCloudCredits().catch(() => null); if (credits) { @@ -896,6 +952,10 @@ export function AppProvider({ children }: { children: ReactNode }) { ...(agentStatus ?? { agentName: "Milaidy", model: undefined, uptime: undefined, startedAt: undefined }), state: "restarting", }); + // Server restart clears in-memory conversations — reset client state + setActiveConversationId(null); + setConversationMessages([]); + setConversations([]); const s = await client.restartAgent(); setAgentStatus(s); } catch { @@ -940,16 +1000,37 @@ export function AppProvider({ children }: { children: ReactNode }) { // ── Chat ─────────────────────────────────────────────────────────── + /** Request an agent greeting for a conversation and add it to messages. */ + const fetchGreeting = useCallback(async (convId: string) => { + setChatSending(true); + try { + const data = await client.requestGreeting(convId); + if (data.text) { + setConversationMessages((prev: ConversationMessage[]) => [ + ...prev, + { id: `greeting-${Date.now()}`, role: "assistant", text: data.text, timestamp: Date.now() }, + ]); + } + } catch { + /* greeting failed silently — user can still chat */ + } finally { + setChatSending(false); + } + }, []); + const handleNewConversation = useCallback(async () => { try { const { conversation } = await client.createConversation(); setConversations((prev) => [conversation, ...prev]); setActiveConversationId(conversation.id); setConversationMessages([]); + // Agent sends the first message + greetingFiredRef.current = true; + void fetchGreeting(conversation.id); } catch { /* ignore */ } - }, []); + }, [fetchGreeting]); const handleChatSend = useCallback(async () => { const text = chatInput.trim(); @@ -980,8 +1061,29 @@ export function AppProvider({ children }: { children: ReactNode }) { ...prev, { id: `temp-resp-${Date.now()}`, role: "assistant", text: data.text, timestamp: Date.now() }, ]); - } catch { - await loadConversationMessages(convId); + } catch (err) { + // If the conversation was lost (server restart), create a fresh one and retry + const status = (err as { status?: number }).status; + if (status === 404) { + try { + const { conversation } = await client.createConversation(); + setConversations((prev) => [conversation, ...prev]); + setActiveConversationId(conversation.id); + convId = conversation.id; + const data = await client.sendConversationMessage(convId, text); + setConversationMessages([ + { id: `temp-${Date.now()}`, role: "user", text, timestamp: Date.now() }, + { id: `temp-resp-${Date.now()}`, role: "assistant", text: data.text, timestamp: Date.now() }, + ]); + } catch { + // Give up — show whatever we have + setConversationMessages([ + { id: `temp-${Date.now()}`, role: "user", text, timestamp: Date.now() }, + ]); + } + } else { + await loadConversationMessages(convId); + } } finally { setChatSending(false); } @@ -1441,6 +1543,9 @@ export function AppProvider({ children }: { children: ReactNode }) { setOnboardingStep("name"); break; case "name": + setOnboardingStep("avatar"); + break; + case "avatar": setOnboardingStep("style"); break; case "style": @@ -1455,10 +1560,8 @@ export function AppProvider({ children }: { children: ReactNode }) { if (onboardingRunMode === "cloud") { if (opts && opts.cloudProviders.length === 1) { setOnboardingCloudProvider(opts.cloudProviders[0].id); - setOnboardingStep("modelSelection"); - } else { - setOnboardingStep("cloudProvider"); } + setOnboardingStep("cloudProvider"); } else { setOnboardingStep("llmProvider"); } @@ -1467,29 +1570,38 @@ export function AppProvider({ children }: { children: ReactNode }) { setOnboardingStep("modelSelection"); break; case "modelSelection": - setOnboardingStep("cloudLogin"); + if (cloudConnected) { + setOnboardingStep("connectors"); + } else { + setOnboardingStep("cloudLogin"); + } break; case "cloudLogin": - // Finish onboarding - await handleOnboardingFinish(); + setOnboardingStep("connectors"); break; case "llmProvider": setOnboardingStep("inventorySetup"); break; case "inventorySetup": + setOnboardingStep("connectors"); + break; + case "connectors": await handleOnboardingFinish(); break; } - }, [onboardingStep, onboardingOptions, onboardingRunMode, onboardingTheme, setTheme]); + }, [onboardingStep, onboardingOptions, onboardingRunMode, onboardingTheme, setTheme, cloudConnected]); const handleOnboardingBack = useCallback(() => { switch (onboardingStep) { case "name": setOnboardingStep("welcome"); break; - case "style": + case "avatar": setOnboardingStep("name"); break; + case "style": + setOnboardingStep("avatar"); + break; case "theme": setOnboardingStep("style"); break; @@ -1500,11 +1612,7 @@ export function AppProvider({ children }: { children: ReactNode }) { setOnboardingStep("runMode"); break; case "modelSelection": - if (onboardingOptions && onboardingOptions.cloudProviders.length > 1) { - setOnboardingStep("cloudProvider"); - } else { - setOnboardingStep("runMode"); - } + setOnboardingStep("cloudProvider"); break; case "cloudLogin": setOnboardingStep("modelSelection"); @@ -1521,8 +1629,16 @@ export function AppProvider({ children }: { children: ReactNode }) { case "inventorySetup": setOnboardingStep("llmProvider"); break; + case "connectors": + // Go back to whichever path we came from + if (onboardingRunMode === "cloud") { + setOnboardingStep("modelSelection"); + } else { + setOnboardingStep("inventorySetup"); + } + break; } - }, [onboardingStep, onboardingOptions]); + }, [onboardingStep, onboardingOptions, onboardingRunMode]); const handleOnboardingFinish = useCallback(async () => { if (!onboardingOptions) return; @@ -1540,6 +1656,7 @@ export function AppProvider({ children }: { children: ReactNode }) { } } + setOnboardingRestarting(true); try { await client.submitOnboarding({ name: onboardingName, @@ -1550,6 +1667,7 @@ export function AppProvider({ children }: { children: ReactNode }) { style: style?.style, adjectives: style?.adjectives, topics: style?.topics, + postExamples: style?.postExamples, messageExamples: style?.messageExamples, cloudProvider: onboardingRunMode === "cloud" ? onboardingCloudProvider : undefined, smallModel: onboardingRunMode === "cloud" ? onboardingSmallModel : undefined, @@ -1557,23 +1675,39 @@ export function AppProvider({ children }: { children: ReactNode }) { provider: onboardingRunMode === "local" ? onboardingProvider || undefined : undefined, providerApiKey: onboardingRunMode === "local" ? onboardingApiKey || undefined : undefined, inventoryProviders: inventoryProviders.length > 0 ? inventoryProviders : undefined, + // Connectors + telegramToken: onboardingTelegramToken.trim() || undefined, + discordToken: onboardingDiscordToken.trim() || undefined, + whatsappSessionPath: onboardingWhatsAppSessionPath.trim() || undefined, + twilioAccountSid: onboardingTwilioAccountSid.trim() || undefined, + twilioAuthToken: onboardingTwilioAuthToken.trim() || undefined, + twilioPhoneNumber: onboardingTwilioPhoneNumber.trim() || undefined, + blooioApiKey: onboardingBlooioApiKey.trim() || undefined, + blooioPhoneNumber: onboardingBlooioPhoneNumber.trim() || undefined, }); } catch (err) { + setOnboardingRestarting(false); window.alert(`Setup failed: ${err instanceof Error ? err.message : "network error"}. Please try again.`); return; } setOnboardingComplete(true); + setTab("chat"); try { setAgentStatus(await client.restartAgent()); } catch { /* ignore */ } + setOnboardingRestarting(false); }, [ onboardingOptions, onboardingStyle, onboardingName, onboardingTheme, onboardingRunMode, onboardingCloudProvider, onboardingSmallModel, onboardingLargeModel, onboardingProvider, onboardingApiKey, onboardingSelectedChains, onboardingRpcSelections, onboardingRpcKeys, + onboardingTelegramToken, onboardingDiscordToken, onboardingWhatsAppSessionPath, + onboardingTwilioAccountSid, onboardingTwilioAuthToken, onboardingTwilioPhoneNumber, + onboardingBlooioApiKey, onboardingBlooioPhoneNumber, + setTab, ]); // ── Cloud ────────────────────────────────────────────────────────── @@ -1600,8 +1734,14 @@ export function AppProvider({ children }: { children: ReactNode }) { if (poll.status === "authenticated") { if (cloudLoginPollTimer.current) clearInterval(cloudLoginPollTimer.current); setCloudLoginBusy(false); + // Immediately reflect the login in the UI — don't wait for the + // background poll which may race with the config save. + setCloudConnected(true); + setCloudEnabled(true); setActionNotice("Logged in to Eliza Cloud successfully.", "success", 6000); - void pollCloudCredits(); + // Delay the credit fetch slightly so the backend has time to + // persist the API key before we query cloud status / credits. + setTimeout(() => void pollCloudCredits(), 2000); } else if (poll.status === "expired" || poll.status === "error") { if (cloudLoginPollTimer.current) clearInterval(cloudLoginPollTimer.current); setCloudLoginError(poll.error ?? "Session expired. Please try again."); @@ -1757,7 +1897,21 @@ export function AppProvider({ children }: { children: ReactNode }) { onboardingApiKey: setOnboardingApiKey as (v: never) => void, onboardingSelectedChains: setOnboardingSelectedChains as (v: never) => void, onboardingRpcSelections: setOnboardingRpcSelections as (v: never) => void, + onboardingOpenRouterModel: setOnboardingOpenRouterModel as (v: never) => void, + onboardingTelegramToken: setOnboardingTelegramToken as (v: never) => void, + onboardingDiscordToken: setOnboardingDiscordToken as (v: never) => void, + onboardingWhatsAppSessionPath: setOnboardingWhatsAppSessionPath as (v: never) => void, + onboardingTwilioAccountSid: setOnboardingTwilioAccountSid as (v: never) => void, + onboardingTwilioAuthToken: setOnboardingTwilioAuthToken as (v: never) => void, + onboardingTwilioPhoneNumber: setOnboardingTwilioPhoneNumber as (v: never) => void, + onboardingBlooioApiKey: setOnboardingBlooioApiKey as (v: never) => void, + onboardingBlooioPhoneNumber: setOnboardingBlooioPhoneNumber as (v: never) => void, + onboardingSubscriptionTab: setOnboardingSubscriptionTab as (v: never) => void, onboardingRpcKeys: setOnboardingRpcKeys as (v: never) => void, + onboardingAvatar: setOnboardingAvatar as (v: never) => void, + onboardingRestarting: setOnboardingRestarting as (v: never) => void, + selectedVrmIndex: setSelectedVrmIndex as (v: never) => void, + customVrmUrl: setCustomVrmUrl as (v: never) => void, commandQuery: setCommandQuery as (v: never) => void, commandActiveIndex: setCommandActiveIndex as (v: never) => void, storeSearch: setStoreSearch as (v: never) => void, @@ -1799,8 +1953,6 @@ export function AppProvider({ children }: { children: ReactNode }) { mcpHeaderInputs: setMcpHeaderInputs as (v: never) => void, droppedFiles: setDroppedFiles as (v: never) => void, shareIngestNotice: setShareIngestNotice as (v: never) => void, - configRaw: setConfigRaw as (v: never) => void, - configText: setConfigText as (v: never) => void, }; const setter = setterMap[key as string]; if (setter) setter(value as never); @@ -1849,7 +2001,8 @@ export function AppProvider({ children }: { children: ReactNode }) { if (authRequired) return; - // Load conversations + // Load conversations — if none exist, create one and request a greeting + let greetConvId: string | null = null; try { const { conversations: c } = await client.listConversations(); setConversations(c); @@ -1859,6 +2012,21 @@ export function AppProvider({ children }: { children: ReactNode }) { try { const { messages } = await client.getConversationMessages(latest.id); setConversationMessages(messages); + // If the latest conversation has no messages, queue a greeting + if (messages.length === 0) { + greetConvId = latest.id; + } + } catch { + /* ignore */ + } + } else { + // First launch — create a conversation and greet + try { + const { conversation } = await client.createConversation(); + setConversations([conversation]); + setActiveConversationId(conversation.id); + setConversationMessages([]); + greetConvId = conversation.id; } catch { /* ignore */ } @@ -1867,6 +2035,29 @@ export function AppProvider({ children }: { children: ReactNode }) { /* ignore */ } + // If the agent is already running and we have a conversation needing a + // greeting, fire it now. Otherwise the agent-state-transition effect + // below will trigger it once the agent starts. + if (greetConvId) { + try { + const s = await client.getStatus(); + if (s.state === "running" && !greetingFiredRef.current) { + greetingFiredRef.current = true; + setChatSending(true); + try { + const data = await client.requestGreeting(greetConvId); + if (data.text) { + setConversationMessages((prev: ConversationMessage[]) => [ + ...prev, + { id: `greeting-${Date.now()}`, role: "assistant", text: data.text, timestamp: Date.now() }, + ]); + } + } catch { /* ignore */ } + setChatSending(false); + } + } catch { /* ignore */ } + } + void loadWorkbench(); // Connect WebSocket @@ -1898,7 +2089,7 @@ export function AppProvider({ children }: { children: ReactNode }) { const urlTab = tabFromPath(window.location.pathname); if (urlTab) { setTabRaw(urlTab); - if (urlTab === "plugins") void loadPlugins(); + if (urlTab === "features") void loadPlugins(); if (urlTab === "skills") void loadSkills(); if (urlTab === "config") { void checkExtensionStatus(); @@ -1907,7 +2098,6 @@ export function AppProvider({ children }: { children: ReactNode }) { void loadUpdateStatus(); void loadPlugins(); } - if (urlTab === "logs") void loadLogs(); if (urlTab === "inventory") void loadInventory(); } }; @@ -1930,6 +2120,25 @@ export function AppProvider({ children }: { children: ReactNode }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // When agent transitions to "running", send a greeting if conversation is empty + useEffect(() => { + const current = agentStatus?.state ?? null; + const prev = prevAgentStateRef.current; + prevAgentStateRef.current = current; + + if (current === "running" && prev !== "running") { + void loadWorkbench(); + + // Agent just started — greet if conversation is empty. + // The greetingFiredRef guard prevents double-greeting when both the + // init mount effect and this state-transition effect race to fire. + if (activeConversationId && conversationMessages.length === 0 && !chatSending && !greetingFiredRef.current) { + greetingFiredRef.current = true; + void fetchGreeting(activeConversationId); + } + } + }, [agentStatus?.state, loadWorkbench, activeConversationId, conversationMessages.length, chatSending, fetchGreeting]); + // ── Context value ────────────────────────────────────────────────── const value: AppContextValue = { @@ -1949,7 +2158,7 @@ export function AppProvider({ children }: { children: ReactNode }) { walletNftsLoading, inventoryView, walletExportData, walletExportVisible, walletApiKeySaving, inventorySort, walletError, characterData, characterLoading, characterSaving, characterSaveSuccess, - characterSaveError, characterDraft, + characterSaveError, characterDraft, selectedVrmIndex, customVrmUrl, cloudEnabled, cloudConnected, cloudCredits, cloudCreditsLow, cloudCreditsCritical, cloudTopUpUrl, cloudUserId, cloudLoginBusy, cloudLoginError, cloudDisconnecting, updateStatus, updateLoading, updateChannelSaving, @@ -1964,8 +2173,12 @@ export function AppProvider({ children }: { children: ReactNode }) { importBusy, importPassword, importFile, importError, importSuccess, onboardingStep, onboardingOptions, onboardingName, onboardingStyle, onboardingTheme, onboardingRunMode, onboardingCloudProvider, onboardingSmallModel, onboardingLargeModel, - onboardingProvider, onboardingApiKey, onboardingSelectedChains, - onboardingRpcSelections, onboardingRpcKeys, + onboardingProvider, onboardingApiKey, onboardingOpenRouterModel, + onboardingTelegramToken, onboardingDiscordToken, onboardingWhatsAppSessionPath, + onboardingTwilioAccountSid, onboardingTwilioAuthToken, onboardingTwilioPhoneNumber, + onboardingBlooioApiKey, onboardingBlooioPhoneNumber, onboardingSubscriptionTab, + onboardingSelectedChains, onboardingRpcSelections, onboardingRpcKeys, + onboardingAvatar, onboardingRestarting, commandPaletteOpen, commandQuery, commandActiveIndex, mcpConfiguredServers, mcpServerStatuses, mcpMarketplaceQuery, mcpMarketplaceResults, mcpMarketplaceLoading, mcpAction, mcpAddingServer, mcpAddingResult, @@ -1973,7 +2186,6 @@ export function AppProvider({ children }: { children: ReactNode }) { droppedFiles, shareIngestNotice, activeGameApp, activeGameDisplayName, activeGameViewerUrl, activeGameSandbox, activeGamePostMessageAuth, - configRaw, configText, // Actions setTab, setTheme, diff --git a/apps/app/src/api-client.ts b/apps/app/src/api-client.ts index 82e1b5c033..7503d31a4d 100644 --- a/apps/app/src/api-client.ts +++ b/apps/app/src/api-client.ts @@ -104,6 +104,7 @@ export interface StylePreset { }; adjectives: string[]; topics: string[]; + postExamples: string[]; messageExamples: MessageExample[][]; } @@ -164,6 +165,15 @@ export interface OnboardingOptions { sharedStyleRules: string; } +/** Configuration for a single messaging connector. */ +export interface ConnectorConfig { + enabled?: boolean; + botToken?: string; + token?: string; + apiKey?: string; + [key: string]: string | boolean | number | string[] | Record | undefined; +} + export interface OnboardingData { name: string; theme: string; @@ -177,6 +187,7 @@ export interface OnboardingData { }; adjectives?: string[]; topics?: string[]; + postExamples?: string[]; messageExamples?: MessageExample[][]; // Cloud-specific cloudProvider?: string; @@ -186,12 +197,25 @@ export interface OnboardingData { provider?: string; providerApiKey?: string; openrouterModel?: string; + subscriptionProvider?: string; + // Messaging channel setup + channels?: Record; // Inventory / wallet setup inventoryProviders?: Array<{ chain: string; rpcProvider: string; rpcApiKey?: string; }>; + // Connector setup (Telegram, Discord, etc.) + connectors?: Record; + telegramToken?: string; + discordToken?: string; + whatsappSessionPath?: string; + twilioAccountSid?: string; + twilioAuthToken?: string; + twilioPhoneNumber?: string; + blooioApiKey?: string; + blooioPhoneNumber?: string; } export interface PluginParamDef { @@ -214,7 +238,7 @@ export interface PluginInfo { enabled: boolean; configured: boolean; envKey: string | null; - category: "ai-provider" | "connector" | "database" | "feature"; + category: "ai-provider" | "connector" | "database" | "app" | "feature"; source: "bundled" | "store"; parameters: PluginParamDef[]; validationErrors: Array<{ field: string; message: string }>; @@ -224,6 +248,20 @@ export interface PluginInfo { pluginDeps?: string[]; } +export interface CorePluginEntry { + npmName: string; + id: string; + name: string; + isCore: boolean; + loaded: boolean; + enabled: boolean; +} + +export interface CorePluginsResponse { + core: CorePluginEntry[]; + optional: CorePluginEntry[]; +} + export interface ChatMessage { role: "user" | "assistant"; text: string; @@ -508,6 +546,28 @@ export interface McpServerStatus { error?: string; } +// Voice / TTS config +export type VoiceProvider = "elevenlabs" | "simple-voice" | "edge"; + +export interface VoiceConfig { + provider?: VoiceProvider; + elevenlabs?: { + apiKey?: string; + voiceId?: string; + modelId?: string; + stability?: number; + similarityBoost?: number; + speed?: number; + }; + edge?: { + voice?: string; + lang?: string; + rate?: string; + pitch?: string; + volume?: string; + }; +} + // Character export interface CharacterData { name?: string; @@ -725,6 +785,38 @@ export class MilaidyClient { }); } + async startAnthropicLogin(): Promise<{ authUrl: string }> { + return this.fetch("/api/subscription/anthropic/start", { method: "POST" }); + } + + async exchangeAnthropicCode(code: string): Promise<{ success: boolean; expiresAt?: string }> { + return this.fetch("/api/subscription/anthropic/exchange", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code }), + }); + } + + async submitAnthropicSetupToken(token: string): Promise<{ success: boolean }> { + return this.fetch("/api/subscription/anthropic/setup-token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token }), + }); + } + + async startOpenAILogin(): Promise<{ authUrl: string; state: string; instructions: string }> { + return this.fetch("/api/subscription/openai/start", { method: "POST" }); + } + + async exchangeOpenAICode(code: string): Promise<{ success: boolean; expiresAt?: string; accountId?: string }> { + return this.fetch("/api/subscription/openai/exchange", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code }), + }); + } + async startAgent(): Promise { const res = await this.fetch<{ status: AgentStatus }>("/api/agent/start", { method: "POST" }); return res.status; @@ -766,17 +858,51 @@ export class MilaidyClient { }); } + // ── Connectors ────────────────────────────────────────────────────── + + async getConnectors(): Promise<{ connectors: Record }> { + return this.fetch("/api/connectors"); + } + + async saveConnector(name: string, config: ConnectorConfig): Promise<{ connectors: Record }> { + return this.fetch("/api/connectors", { + method: "POST", + body: JSON.stringify({ name, config }), + }); + } + + async deleteConnector(name: string): Promise<{ connectors: Record }> { + return this.fetch(`/api/connectors/${encodeURIComponent(name)}`, { + method: "DELETE", + }); + } + async getPlugins(): Promise<{ plugins: PluginInfo[] }> { return this.fetch("/api/plugins"); } - async updatePlugin(id: string, config: Record): Promise { - await this.fetch(`/api/plugins/${id}`, { + async getCorePlugins(): Promise { + return this.fetch("/api/plugins/core"); + } + + async toggleCorePlugin(npmName: string, enabled: boolean): Promise<{ ok: boolean; restarting?: boolean; message?: string }> { + return this.fetch("/api/plugins/core/toggle", { + method: "POST", + body: JSON.stringify({ npmName, enabled }), + }); + } + + async updatePlugin(id: string, config: Record): Promise<{ ok: boolean; restarting?: boolean }> { + return this.fetch(`/api/plugins/${id}`, { method: "PUT", body: JSON.stringify(config), }); } + async restart(): Promise<{ ok: boolean }> { + return this.fetch("/api/restart", { method: "POST" }); + } + async getSkills(): Promise<{ skills: SkillInfo[] }> { return this.fetch("/api/skills"); } @@ -1304,6 +1430,12 @@ export class MilaidyClient { }); } + async requestGreeting(id: string): Promise<{ text: string; agentName: string; generated: boolean }> { + return this.fetch(`/api/conversations/${encodeURIComponent(id)}/greeting`, { + method: "POST", + }); + } + async renameConversation(id: string, title: string): Promise<{ conversation: Conversation }> { return this.fetch(`/api/conversations/${encodeURIComponent(id)}`, { method: "PATCH", diff --git a/apps/app/src/bridge/index.ts b/apps/app/src/bridge/index.ts deleted file mode 100644 index f8196c0097..0000000000 --- a/apps/app/src/bridge/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Capacitor Bridge Module - * - * This module exports all bridge utilities for use by the app and plugins. - */ - -export { - initializeCapacitorBridge, - getCapabilities, - haptics, - registerPlugin, - getPlugin, - hasPlugin, - waitForBridge, - type MilaidyBridge, - type CapacitorCapabilities, -} from "./capacitor-bridge.js"; - -export { - initializeStorageBridge, - getStorageValue, - setStorageValue, - removeStorageValue, - registerSyncedKey, - isStorageBridgeInitialized, -} from "./storage-bridge.js"; diff --git a/apps/app/src/bridge/plugin-bridge.ts b/apps/app/src/bridge/plugin-bridge.ts index 7aaab825ae..d2ce9c7e64 100644 --- a/apps/app/src/bridge/plugin-bridge.ts +++ b/apps/app/src/bridge/plugin-bridge.ts @@ -19,8 +19,6 @@ import { Location as LocationPlugin } from "@milaidy/capacitor-location"; import { ScreenCapture as ScreenCapturePlugin } from "@milaidy/capacitor-screencapture"; import { Canvas as CanvasPlugin } from "@milaidy/capacitor-canvas"; import { Desktop as DesktopPlugin } from "@milaidy/capacitor-desktop"; -import { Agent as AgentPlugin } from "@milaidy/capacitor-agent"; - // Import types import type { GatewayPlugin as IGatewayPlugin } from "@milaidy/capacitor-gateway"; import type { SwabblePlugin as ISwabblePlugin } from "@milaidy/capacitor-swabble"; diff --git a/apps/app/src/components/AdminView.tsx b/apps/app/src/components/AdminView.tsx new file mode 100644 index 0000000000..d01b91eb33 --- /dev/null +++ b/apps/app/src/components/AdminView.tsx @@ -0,0 +1,220 @@ +/** + * Admin view — logs, database management, and core plugin status. + * + * Contains three sub-tabs: + * - Logs: agent runtime logs + * - Plugins: core plugin status & optional plugin toggles + * - Database: database explorer + */ + +import { useCallback, useEffect, useState } from "react"; +import { client } from "../api-client"; +import type { CorePluginEntry } from "../api-client"; +import { LogsView } from "./LogsView"; +import { DatabaseView } from "./DatabaseView"; + +type AdminTab = "logs" | "plugins" | "database"; + +const ADMIN_TABS: { id: AdminTab; label: string }[] = [ + { id: "logs", label: "Logs" }, + { id: "plugins", label: "Plugins" }, + { id: "database", label: "Database" }, +]; + +/* ── Core Plugins sub-view ──────────────────────────────────────────── */ + +function CorePluginsView() { + const [core, setCore] = useState([]); + const [optional, setOptional] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [toggling, setToggling] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data = await client.getCorePlugins(); + setCore(data.core); + setOptional(data.optional); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load"); + } + setLoading(false); + }, []); + + useEffect(() => { void load(); }, [load]); + + const handleToggle = useCallback(async (plugin: CorePluginEntry) => { + setToggling(plugin.id); + try { + await client.toggleCorePlugin(plugin.npmName, !plugin.enabled); + // Optimistic update + setOptional(prev => + prev.map(p => + p.id === plugin.id ? { ...p, enabled: !p.enabled } : p, + ), + ); + } catch (err) { + setError(err instanceof Error ? err.message : "Toggle failed"); + } + setToggling(null); + }, []); + + if (loading) { + return
Loading plugin status...
; + } + if (error) { + return ( +
+ {error} + +
+ ); + } + + const loadedCore = core.filter(p => p.loaded).length; + const enabledOptional = optional.filter(p => p.enabled).length; + + return ( +
+ {/* Summary */} +
+ {loadedCore}/{core.length} core running · {enabledOptional}/{optional.length} optional enabled + +
+ + {/* Core plugins */} +
+

Core Plugins

+

Always loaded. Required for the agent to function.

+
+ {core.map(p => ( + + ))} +
+
+ + {/* Optional plugins */} +
+

Optional Plugins

+

+ Toggle to enable or disable. Agent will restart automatically. +

+
+ {optional.map(p => ( + handleToggle(p)} + /> + ))} +
+
+
+ ); +} + +function PluginRow({ + plugin, + toggleable, + toggling, + onToggle, +}: { + plugin: CorePluginEntry; + toggleable?: boolean; + toggling?: boolean; + onToggle?: () => void; +}) { + return ( +
+ {/* Status dot */} + + + {/* Name */} + + {plugin.name} + + + {/* Status label */} + + {plugin.loaded ? "Running" : plugin.enabled ? "Enabled" : "Off"} + + + {/* Toggle button for optional plugins */} + {toggleable && onToggle && ( + + )} +
+ ); +} + +/* ── Main AdminView ─────────────────────────────────────────────────── */ + +export function AdminView() { + const [activeTab, setActiveTab] = useState("logs"); + + return ( +
+ {/* Sub-tab bar */} +
+ {ADMIN_TABS.map((t) => ( + + ))} +
+ + {/* Sub-tab content */} + {activeTab === "logs" && } + {activeTab === "plugins" && } + {activeTab === "database" && } +
+ ); +} diff --git a/apps/app/src/components/AppsView.tsx b/apps/app/src/components/AppsView.tsx index c43ec0ca9c..26b7f5592e 100644 --- a/apps/app/src/components/AppsView.tsx +++ b/apps/app/src/components/AppsView.tsx @@ -47,7 +47,7 @@ export function AppsView() { setState("activeGameApp", app.name); setState("activeGameDisplayName", app.displayName ?? app.name); setState("activeGameViewerUrl", result.viewer.url); - setState("tab", "game" as never); + setState("tab", "game"); } } catch (err) { setActionNotice( diff --git a/apps/app/src/components/AvatarSelector.tsx b/apps/app/src/components/AvatarSelector.tsx new file mode 100644 index 0000000000..6225066145 --- /dev/null +++ b/apps/app/src/components/AvatarSelector.tsx @@ -0,0 +1,94 @@ +/** + * Reusable avatar/character VRM selector. + * + * Shows a single row of the 8 built-in milady VRMs as thumbnail images. + * The selected avatar gets a highlight ring. No text labels. + */ + +import { useRef } from "react"; +import { VRM_COUNT, getVrmPreviewUrl } from "../AppContext"; + +export interface AvatarSelectorProps { + /** Currently selected index (1-8 for built-in, 0 for custom) */ + selected: number; + /** Called when a built-in avatar is selected */ + onSelect: (index: number) => void; + /** Called when a custom VRM is uploaded */ + onUpload?: (file: File) => void; + /** Whether to show the upload option */ + showUpload?: boolean; +} + +export function AvatarSelector({ + selected, + onSelect, + onUpload, + showUpload = true, +}: AvatarSelectorProps) { + const fileInputRef = useRef(null); + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + if (!file.name.endsWith(".vrm")) { + alert("Please select a .vrm file"); + return; + } + onUpload?.(file); + onSelect(0); // 0 = custom + }; + + const avatarIndices = Array.from({ length: VRM_COUNT }, (_, i) => i + 1); + + return ( +
+
+ {avatarIndices.map((i) => ( + + ))} + + {/* Upload custom VRM */} + {showUpload && ( + <> + + + + )} +
+
+ ); +} diff --git a/apps/app/src/components/CharacterView.tsx b/apps/app/src/components/CharacterView.tsx new file mode 100644 index 0000000000..bcc9820f8a --- /dev/null +++ b/apps/app/src/components/CharacterView.tsx @@ -0,0 +1,1029 @@ +/** + * Character view — agent identity, personality, and avatar. + * + * Features: + * - Import/export character as JSON + * - Unsaved changes indicator + * - Adjectives and topics editors + */ + +import { useEffect, useState, useCallback, useRef } from "react"; +import { useApp } from "../AppContext"; +import { client, type VoiceProvider, type VoiceConfig } from "../api-client"; +import { AvatarSelector } from "./AvatarSelector"; + +export function CharacterView() { + const { + characterData, + characterDraft, + characterLoading, + characterSaving, + characterSaveSuccess, + characterSaveError, + handleCharacterFieldInput, + handleCharacterArrayInput, + handleCharacterStyleInput, + handleSaveCharacter, + loadCharacter, + setState, + selectedVrmIndex, + // Cloud (for ElevenLabs via Eliza Cloud) + cloudConnected, + cloudUserId, + cloudLoginBusy, + cloudLoginError, + cloudDisconnecting, + handleCloudLogin, + handleCloudDisconnect, + } = useApp(); + + useEffect(() => { + void loadCharacter(); + }, [loadCharacter]); + + const fileInputRef = useRef(null); + + const handleFieldEdit = useCallback((field: string, value: string | string[] | Record[]) => { + handleCharacterFieldInput(field as never, value as never); + }, [handleCharacterFieldInput]); + + const handleStyleEdit = useCallback((key: "all" | "chat" | "post", value: string) => { + handleCharacterStyleInput(key, value); + }, [handleCharacterStyleInput]); + + /* ── Import / Export ────────────────────────────────────────────── */ + const handleExport = useCallback(() => { + const d = characterDraft; + const exportData = { + name: d.name ?? "", + bio: typeof d.bio === "string" ? d.bio.split("\n").filter(Boolean) : (d.bio ?? []), + system: d.system ?? "", + style: { + all: d.style?.all ?? [], + chat: d.style?.chat ?? [], + post: d.style?.post ?? [], + }, + adjectives: d.adjectives ?? [], + topics: d.topics ?? [], + messageExamples: (d.messageExamples ?? []).map((convo: any) => + (convo.examples ?? []).map((msg: any) => ({ + user: msg.name, + content: { text: msg.content?.text ?? "" }, + })) + ), + postExamples: d.postExamples ?? [], + }; + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${(d.name ?? "character").toLowerCase().replace(/\s+/g, "-")}.character.json`; + a.click(); + URL.revokeObjectURL(url); + }, [characterDraft]); + + const handleImport = useCallback((e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + try { + const data = JSON.parse(reader.result as string); + if (data.name) handleCharacterFieldInput("name", data.name); + if (data.bio) handleCharacterFieldInput("bio", + Array.isArray(data.bio) ? data.bio.join("\n") : data.bio); + if (data.system) handleCharacterFieldInput("system", data.system); + if (data.adjectives) handleCharacterFieldInput("adjectives" as any, data.adjectives); + if (data.topics) handleCharacterFieldInput("topics" as any, data.topics); + if (data.style) { + if (data.style.all) handleCharacterStyleInput("all", + Array.isArray(data.style.all) ? data.style.all.join("\n") : data.style.all); + if (data.style.chat) handleCharacterStyleInput("chat", + Array.isArray(data.style.chat) ? data.style.chat.join("\n") : data.style.chat); + if (data.style.post) handleCharacterStyleInput("post", + Array.isArray(data.style.post) ? data.style.post.join("\n") : data.style.post); + } + if (data.messageExamples) { + const formatted = data.messageExamples.map((convo: any[]) => ({ + examples: convo.map((msg: any) => ({ + name: msg.user ?? msg.name ?? "{{user1}}", + content: { text: msg.content?.text ?? msg.text ?? "" }, + })), + })); + handleCharacterFieldInput("messageExamples" as any, formatted); + } + if (data.postExamples) handleCharacterFieldInput("postExamples" as any, data.postExamples); + } catch { + alert("invalid json file"); + } + }; + reader.readAsText(file); + // Reset input so same file can be re-imported + e.target.value = ""; + }, [handleCharacterFieldInput, handleCharacterStyleInput]); + + /* ── Character generation state ─────────────────────────────────── */ + const [generating, setGenerating] = useState(null); + + /* ── Voice config state ─────────────────────────────────────────── */ + const [voiceProvider, setVoiceProvider] = useState("elevenlabs"); + const [voiceConfig, setVoiceConfig] = useState({}); + const [voiceLoading, setVoiceLoading] = useState(false); + const [voiceSaving, setVoiceSaving] = useState(false); + const [voiceSaveSuccess, setVoiceSaveSuccess] = useState(false); + const [voiceSaveError, setVoiceSaveError] = useState(null); + const [voiceMode, setVoiceMode] = useState<"cloud" | "own-key">("cloud"); + const [voiceTesting, setVoiceTesting] = useState(false); + const [voiceTestAudio, setVoiceTestAudio] = useState(null); + const [selectedPresetId, setSelectedPresetId] = useState(null); + + /* ── ElevenLabs voice presets ──────────────────────────────────── */ + type VoicePreset = { id: string; name: string; voiceId: string; gender: "female" | "male" | "character"; hint: string; previewUrl: string }; + const VOICE_PRESETS: VoicePreset[] = [ + // Female + { id: "rachel", name: "Rachel", voiceId: "21m00Tcm4TlvDq8ikWAM", gender: "female", hint: "Calm, clear", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/21m00Tcm4TlvDq8ikWAM/df6788f9-5c96-470d-8312-aab3b3d8f50a.mp3" }, + { id: "sarah", name: "Sarah", voiceId: "EXAVITQu4vr4xnSDxMaL", gender: "female", hint: "Soft, warm", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/EXAVITQu4vr4xnSDxMaL/6851ec91-9950-471f-8586-357c52539069.mp3" }, + { id: "matilda", name: "Matilda", voiceId: "XrExE9yKIg1WjnnlVkGX", gender: "female", hint: "Warm, friendly", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/XrExE9yKIg1WjnnlVkGX/b930e18d-6b4d-466e-bab2-0ae97c6d8535.mp3" }, + { id: "lily", name: "Lily", voiceId: "pFZP5JQG7iQjIQuC4Bku", gender: "female", hint: "British, raspy", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/pFZP5JQG7iQjIQuC4Bku/0ab8bd74-fcd2-489d-b70a-3e1bcde8c999.mp3" }, + { id: "alice", name: "Alice", voiceId: "Xb7hH8MSUJpSbSDYk0k2", gender: "female", hint: "British, confident", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/Xb7hH8MSUJpSbSDYk0k2/f5409e2f-d9c3-4ac9-9e7d-916a5dbd1ef1.mp3" }, + // Male + { id: "brian", name: "Brian", voiceId: "nPczCjzI2devNBz1zQrb", gender: "male", hint: "Deep, smooth", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/nPczCjzI2devNBz1zQrb/f4dbda0c-aff0-45c0-93fa-f5d5ec95a2eb.mp3" }, + { id: "adam", name: "Adam", voiceId: "pNInz6obpgDQGcFmaJgB", gender: "male", hint: "Deep, authoritative", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/pNInz6obpgDQGcFmaJgB/38a69695-2ca9-4b9e-b9ec-f07ced494a58.mp3" }, + { id: "josh", name: "Josh", voiceId: "TxGEqnHWrfWFTfGW9XjX", gender: "male", hint: "Young, deep", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/TxGEqnHWrfWFTfGW9XjX/3ae2fc71-d5f9-4769-bb71-2a43633cd186.mp3" }, + { id: "daniel", name: "Daniel", voiceId: "onwK4e9ZLuTAKqWW03F9", gender: "male", hint: "British, presenter", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/onwK4e9ZLuTAKqWW03F9/7eee0236-1a72-4b86-b303-5dcadc007ba9.mp3" }, + { id: "liam", name: "Liam", voiceId: "TX3LPaxmHKxFdv7VOQHJ", gender: "male", hint: "Young, natural", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/TX3LPaxmHKxFdv7VOQHJ/63148076-6363-42db-aea8-31424308b92c.mp3" }, + // Character / Cutesy / Game + { id: "gigi", name: "Gigi", voiceId: "jBpfuIE2acCO8z3wKNLl", gender: "character", hint: "Childish, cute", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/jBpfuIE2acCO8z3wKNLl/3a7e4339-78fa-404e-8d10-c3ef5587935b.mp3" }, + { id: "mimi", name: "Mimi", voiceId: "zrHiDhphv9ZnVXBqCLjz", gender: "character", hint: "Cute, animated", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/zrHiDhphv9ZnVXBqCLjz/decbf20b-0f57-4fac-985b-a4f0290ebfc4.mp3" }, + { id: "dorothy", name: "Dorothy", voiceId: "ThT5KcBeYPX3keUQqHPh", gender: "character", hint: "Sweet, storybook", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/ThT5KcBeYPX3keUQqHPh/981f0855-6598-48d2-9f8f-b6d92fbbe3fc.mp3" }, + { id: "glinda", name: "Glinda", voiceId: "z9fAnlkpzviPz146aGWa", gender: "character", hint: "Magical, whimsical", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/z9fAnlkpzviPz146aGWa/cbc60443-7b61-4ebb-b8e1-5c03237ea01d.mp3" }, + { id: "charlotte", name: "Charlotte", voiceId: "XB0fDUnXU5powFXDhCwa", gender: "character", hint: "Alluring, game NPC", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/XB0fDUnXU5powFXDhCwa/942356dc-f10d-4d89-bda5-4f8505ee038b.mp3" }, + { id: "callum", name: "Callum", voiceId: "N2lVS1w4EtoT3dr4eOWO", gender: "character", hint: "Gruff, game hero", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/N2lVS1w4EtoT3dr4eOWO/ac833bd8-ffda-4938-9ebc-b0f99ca25481.mp3" }, + ]; + + /* Load voice config on mount */ + useEffect(() => { + void (async () => { + setVoiceLoading(true); + try { + const cfg = await client.getConfig(); + const messages = cfg.messages as Record> | undefined; + const tts = messages?.tts as VoiceConfig | undefined; + if (tts?.provider) setVoiceProvider(tts.provider); + if (tts) { + setVoiceConfig(tts); + // Detect voice mode: if user has own API key set, default to own-key mode + if (tts.elevenlabs?.apiKey) setVoiceMode("own-key"); + // Detect selected preset + if (tts.elevenlabs?.voiceId) { + const preset = VOICE_PRESETS.find((p) => p.voiceId === tts.elevenlabs?.voiceId); + setSelectedPresetId(preset?.id ?? "custom"); + } + } + } catch { /* ignore */ } + setVoiceLoading(false); + })(); + }, []); + + const handleVoiceFieldChange = useCallback( + (provider: "elevenlabs" | "edge", key: string, value: string | number) => { + setVoiceConfig((prev) => ({ + ...prev, + [provider]: { ...(prev[provider] ?? {}), [key]: value }, + })); + }, + [], + ); + + const handleSelectPreset = useCallback( + (preset: VoicePreset) => { + setSelectedPresetId(preset.id); + setVoiceConfig((prev) => ({ + ...prev, + elevenlabs: { ...(prev.elevenlabs ?? {}), voiceId: preset.voiceId }, + })); + }, + [], + ); + + const handleTestVoice = useCallback( + (previewUrl: string) => { + // Stop any existing playback + if (voiceTestAudio) { + voiceTestAudio.pause(); + voiceTestAudio.currentTime = 0; + } + setVoiceTesting(true); + const audio = new Audio(previewUrl); + setVoiceTestAudio(audio); + audio.onended = () => setVoiceTesting(false); + audio.onerror = () => setVoiceTesting(false); + audio.play().catch(() => setVoiceTesting(false)); + }, + [voiceTestAudio], + ); + + const handleStopTest = useCallback(() => { + if (voiceTestAudio) { + voiceTestAudio.pause(); + voiceTestAudio.currentTime = 0; + } + setVoiceTesting(false); + }, [voiceTestAudio]); + + const handleVoiceSave = useCallback(async () => { + setVoiceSaving(true); + setVoiceSaveError(null); + setVoiceSaveSuccess(false); + try { + const elConfig = { ...(voiceConfig.elevenlabs ?? {}) }; + // In cloud mode, don't send apiKey — cloud handles it + if (voiceMode === "cloud") { + delete elConfig.apiKey; + } + await client.updateConfig({ + messages: { + tts: { + provider: voiceProvider, + ...(voiceProvider === "elevenlabs" ? { elevenlabs: elConfig } : {}), + ...(voiceProvider === "edge" && voiceConfig.edge + ? { edge: voiceConfig.edge } + : {}), + }, + }, + }); + setVoiceSaveSuccess(true); + setTimeout(() => setVoiceSaveSuccess(false), 2500); + } catch (err) { + setVoiceSaveError(err instanceof Error ? err.message : "Failed to save — is the agent running?"); + } + setVoiceSaving(false); + }, [voiceProvider, voiceConfig, voiceMode]); + + const d = characterDraft; + const bioText = typeof d.bio === "string" ? d.bio : Array.isArray(d.bio) ? d.bio.join("\n") : ""; + const styleAllText = (d.style?.all ?? []).join("\n"); + const styleChatText = (d.style?.chat ?? []).join("\n"); + const stylePostText = (d.style?.post ?? []).join("\n"); + + const getCharContext = useCallback(() => ({ + name: d.name ?? "", + system: d.system ?? "", + bio: bioText, + style: d.style ?? { all: [], chat: [], post: [] }, + postExamples: d.postExamples ?? [], + }), [d, bioText]); + + const handleGenerate = useCallback(async (field: string, mode: "append" | "replace" = "replace") => { + setGenerating(field); + try { + const { generated } = await client.generateCharacterField(field, getCharContext(), mode); + if (field === "bio") { + handleFieldEdit("bio", generated.trim()); + } else if (field === "style") { + try { + const parsed = JSON.parse(generated); + if (mode === "append") { + handleStyleEdit("all", [...(d.style?.all ?? []), ...(parsed.all ?? [])].join("\n")); + handleStyleEdit("chat", [...(d.style?.chat ?? []), ...(parsed.chat ?? [])].join("\n")); + handleStyleEdit("post", [...(d.style?.post ?? []), ...(parsed.post ?? [])].join("\n")); + } else { + if (parsed.all) handleStyleEdit("all", parsed.all.join("\n")); + if (parsed.chat) handleStyleEdit("chat", parsed.chat.join("\n")); + if (parsed.post) handleStyleEdit("post", parsed.post.join("\n")); + } + } catch { /* raw text fallback */ } + } else if (field === "chatExamples") { + try { + const parsed = JSON.parse(generated); + if (Array.isArray(parsed)) { + const formatted = parsed.map((convo: Array<{ user: string; content: { text: string } }>) => ({ + examples: convo.map((msg) => ({ name: msg.user, content: { text: msg.content.text } })), + })); + handleFieldEdit("messageExamples", formatted); + } + } catch { /* raw text fallback */ } + } else if (field === "postExamples") { + try { + const parsed = JSON.parse(generated); + if (Array.isArray(parsed)) { + if (mode === "append") { + handleCharacterArrayInput("postExamples", [...(d.postExamples ?? []), ...parsed].join("\n")); + } else { + handleCharacterArrayInput("postExamples", parsed.join("\n")); + } + } + } catch { /* raw text fallback */ } + } + } catch { /* generation failed */ } + setGenerating(null); + }, [getCharContext, d, handleFieldEdit, handleStyleEdit, handleCharacterArrayInput]); + + const handleRandomName = useCallback(async () => { + try { + const { name } = await client.getRandomName(); + handleFieldEdit("name", name); + } catch { /* ignore */ } + }, [handleFieldEdit]); + + /* ── Helpers ────────────────────────────────────────────────────── */ + const inputCls = "px-2.5 py-1.5 border border-[var(--border)] bg-[var(--card)] text-xs focus:border-[var(--accent)] focus:outline-none"; + const textareaCls = `${inputCls} font-inherit resize-y leading-relaxed`; + const labelCls = "font-semibold text-xs"; + const hintCls = "text-[11px] text-[var(--muted)]"; + const tinyBtnCls = "text-[10px] px-1.5 py-0.5 border border-[var(--border)] bg-[var(--card)] cursor-pointer hover:border-[var(--accent)] hover:text-[var(--accent)] transition-colors disabled:opacity-40"; + + return ( +
+
+ {characterLoading && !characterData ? ( +
+ loading character data... +
+ ) : ( +
+ {/* Name + reload */} +
+
+ + +
+
+ handleFieldEdit("name", e.target.value)} + className={inputCls + " flex-1 text-[13px]"} + /> + +
+
+ + {/* Avatar (below name, full width) */} +
+ + setState("selectedVrmIndex", i)} + onUpload={(file) => { + const url = URL.createObjectURL(file); + setState("customVrmUrl", url); + setState("selectedVrmIndex", 0); + }} + showUpload + /> +
+ + {/* Identity (bio) */} +
+
+ + +
+