Compare commits
12 Commits
8ba19ef0c4
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c5bbf63a5 | |||
| 6c2c3a0d07 | |||
| 5018311990 | |||
| 82f29dceae | |||
| 0595e72f7f | |||
| 7b2621c264 | |||
| 09a59f170c | |||
| 8c2b80951b | |||
| c5c070ebc2 | |||
| 1d68638e69 | |||
| ea7b97198f | |||
| 46e84e4705 |
@@ -1,11 +1,14 @@
|
|||||||
|
FROM node:22-slim
|
||||||
FROM node:22-alpine
|
|
||||||
|
|
||||||
ENV PORT=80
|
ENV PORT=80
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci
|
RUN apt-get update && apt-get install -y --no-install-recommends libpython3.11 python3 python3-pip && \
|
||||||
|
npm ci && rm -rf /root/.npm/* /root/.cache/* /tmp/* /var/lib/apt/lists/* && \
|
||||||
|
echo 'alias sudo=""' >> /root/.bashrc && \
|
||||||
|
echo 'alias pip="pip3"' >> /root/.bashrc && \
|
||||||
|
echo 'alias python="python3"' >> /root/.bashrc
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
@@ -76,6 +76,13 @@ Your personal AI network navigator
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
- Use zips for protocols - move avatar to protocol zip
|
||||||
|
- Home screen corner menu
|
||||||
|
|
||||||
|
- Notifications
|
||||||
|
- CRON / Schedule
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Copyright © 2023 Zakary Timson | All Rights Reserved | Available under MIT Licensing
|
Copyright © 2023 Zakary Timson | All Rights Reserved | Available under MIT Licensing
|
||||||
|
|
||||||
|
|||||||
604
package-lock.json
generated
@@ -8,9 +8,12 @@
|
|||||||
"name": "@ztimson/net-navi",
|
"name": "@ztimson/net-navi",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ztimson/ai-utils": "^0.8.5",
|
"@ztimson/ai-utils": "^0.8.9",
|
||||||
|
"@ztimson/utils": "^0.28.14",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"socket.io": "^4.6.1"
|
"socket.io": "^4.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -43,6 +46,16 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@huggingface/jinja": {
|
"node_modules/@huggingface/jinja": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz",
|
||||||
@@ -52,6 +65,471 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/colour": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-ppc64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-riscv64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-wasm32": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/runtime": "^1.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-ia32": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@protobufjs/aspromise": {
|
"node_modules/@protobufjs/aspromise": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||||
@@ -301,10 +779,33 @@
|
|||||||
"onnxruntime-node": "1.14.0"
|
"onnxruntime-node": "1.14.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xenova/transformers/node_modules/sharp": {
|
||||||
|
"version": "0.32.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
|
||||||
|
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"color": "^4.2.3",
|
||||||
|
"detect-libc": "^2.0.2",
|
||||||
|
"node-addon-api": "^6.1.0",
|
||||||
|
"prebuild-install": "^7.1.1",
|
||||||
|
"semver": "^7.5.4",
|
||||||
|
"simple-get": "^4.0.1",
|
||||||
|
"tar-fs": "^3.0.4",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.15.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ztimson/ai-utils": {
|
"node_modules/@ztimson/ai-utils": {
|
||||||
"version": "0.8.5",
|
"version": "0.8.9",
|
||||||
"resolved": "https://registry.npmjs.org/@ztimson/ai-utils/-/ai-utils-0.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/@ztimson/ai-utils/-/ai-utils-0.8.9.tgz",
|
||||||
"integrity": "sha512-MIIg03NwUm5v0vHihMDYLaFK8DkuLh1G5lmQpVyZif0h4s2wh9eO7uUg6E7c0UtvSq5QDh/eCyEDd5HvHxq01w==",
|
"integrity": "sha512-BHaJjtFS+qF/LZJinscmHPtef1jq/6xmPXw6kKbSYLmcQJLmVoQg5NJXU2EQjaYtc36OuYXeM98nWBBYWl6hdA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.78.0",
|
"@anthropic-ai/sdk": "^0.78.0",
|
||||||
@@ -324,9 +825,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@ztimson/utils": {
|
"node_modules/@ztimson/utils": {
|
||||||
"version": "0.28.13",
|
"version": "0.28.14",
|
||||||
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.28.13.tgz",
|
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.28.14.tgz",
|
||||||
"integrity": "sha512-6nk7mW1vPX5QltTjCNK6u6y8UGYTOgeDadWHbrD+1QM5PZ1PbosMqVMQ6l4dhsQT7IHFxvLI3+U720T2fLGwoA==",
|
"integrity": "sha512-ZI8kT1CV9La22w/E+HoPOsSV0ewgz0Rl4LXKIxFxH2dJVkXDCajgjfSHEfIHniifeOMma2nF+VO6yqT9BtvorQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"var-persist": "^1.0.1"
|
"var-persist": "^1.0.1"
|
||||||
@@ -423,7 +924,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
|
||||||
"integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
|
"integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bare-events": "^2.5.4",
|
"bare-events": "^2.5.4",
|
||||||
"bare-path": "^3.0.0",
|
"bare-path": "^3.0.0",
|
||||||
@@ -444,11 +944,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bare-os": {
|
"node_modules/bare-os": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz",
|
||||||
"integrity": "sha512-64Rcwj8qlnTZU8Ps6JJEdSmxBEUGgI7g8l+lMtsJLl4IsfTcHMTfJ188u2iGV6P6YPRZrtv72B2kjn+hp+Yv3g==",
|
"integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"bare": ">=1.14.0"
|
"bare": ">=1.14.0"
|
||||||
}
|
}
|
||||||
@@ -458,7 +957,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bare-os": "^3.0.1"
|
"bare-os": "^3.0.1"
|
||||||
}
|
}
|
||||||
@@ -468,7 +966,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz",
|
||||||
"integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==",
|
"integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"streamx": "^2.21.0",
|
"streamx": "^2.21.0",
|
||||||
"teex": "^1.0.1"
|
"teex": "^1.0.1"
|
||||||
@@ -491,7 +988,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
|
||||||
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
|
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bare-path": "^3.0.0"
|
"bare-path": "^3.0.0"
|
||||||
}
|
}
|
||||||
@@ -979,6 +1475,18 @@
|
|||||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "17.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||||
|
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -2040,9 +2548,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"end-of-stream": "^1.1.0",
|
"end-of-stream": "^1.1.0",
|
||||||
@@ -2228,26 +2736,47 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.32.6",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
|
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color": "^4.2.3",
|
"@img/colour": "^1.0.0",
|
||||||
"detect-libc": "^2.0.2",
|
"detect-libc": "^2.1.2",
|
||||||
"node-addon-api": "^6.1.0",
|
"semver": "^7.7.3"
|
||||||
"prebuild-install": "^7.1.1",
|
|
||||||
"semver": "^7.5.4",
|
|
||||||
"simple-get": "^4.0.1",
|
|
||||||
"tar-fs": "^3.0.4",
|
|
||||||
"tunnel-agent": "^0.6.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.15.0"
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-darwin-arm64": "0.34.5",
|
||||||
|
"@img/sharp-darwin-x64": "0.34.5",
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||||
|
"@img/sharp-linux-arm": "0.34.5",
|
||||||
|
"@img/sharp-linux-arm64": "0.34.5",
|
||||||
|
"@img/sharp-linux-ppc64": "0.34.5",
|
||||||
|
"@img/sharp-linux-riscv64": "0.34.5",
|
||||||
|
"@img/sharp-linux-s390x": "0.34.5",
|
||||||
|
"@img/sharp-linux-x64": "0.34.5",
|
||||||
|
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||||
|
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||||
|
"@img/sharp-wasm32": "0.34.5",
|
||||||
|
"@img/sharp-win32-arm64": "0.34.5",
|
||||||
|
"@img/sharp-win32-ia32": "0.34.5",
|
||||||
|
"@img/sharp-win32-x64": "0.34.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
@@ -2583,12 +3112,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-stream": {
|
"node_modules/tar-stream": {
|
||||||
"version": "3.1.7",
|
"version": "3.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
|
||||||
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
"integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.4",
|
"b4a": "^1.6.4",
|
||||||
|
"bare-fs": "^4.5.5",
|
||||||
"fast-fifo": "^1.2.0",
|
"fast-fifo": "^1.2.0",
|
||||||
"streamx": "^2.15.0"
|
"streamx": "^2.15.0"
|
||||||
}
|
}
|
||||||
@@ -2598,7 +3128,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
|
||||||
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
|
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"streamx": "^2.12.5"
|
"streamx": "^2.12.5"
|
||||||
}
|
}
|
||||||
@@ -2657,6 +3186,13 @@
|
|||||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/tunnel-agent": {
|
"node_modules/tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@ztimson/net-navi",
|
"name": "@ztimson/net-navi",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"description": "Network Navigation Program",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ztimson/ai-utils": "^0.8.5",
|
"@ztimson/ai-utils": "^0.8.9",
|
||||||
|
"@ztimson/utils": "^0.28.14",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"socket.io": "^4.6.1"
|
"socket.io": "^4.6.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
237
public/components/avatar.mjs
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import {TTS} from '../tts.mjs';
|
||||||
|
|
||||||
|
class AvatarComponent extends HTMLElement {
|
||||||
|
|
||||||
|
static get observedAttributes() { return []; }
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({mode: 'open'});
|
||||||
|
this.activeEmotes = [];
|
||||||
|
this.mouthSvg = null;
|
||||||
|
this.mouthState = 'closed';
|
||||||
|
this.setupMouthAnimation();
|
||||||
|
this.navi = window.navi;
|
||||||
|
this.navi.animations().then(animations => {
|
||||||
|
this.animations = animations;
|
||||||
|
if(!this.animations) return console.error(`Invalid animations: ${this.animations}`);
|
||||||
|
this.render(this.animations);
|
||||||
|
navi.on('emote', emote => this.emote(emote));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.emote = this.emote.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(data) {
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
pointer-events: none;
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
.avatar-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: drop-shadow(2px 4px 6px black);
|
||||||
|
}
|
||||||
|
.emote-overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.mouth-overlay {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="avatar-container">
|
||||||
|
<img src="${this.navi.avatar}" class="avatar" alt="Avatar">
|
||||||
|
</div>`;
|
||||||
|
this.loadMouthSvg();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMouthSvg() {
|
||||||
|
fetch('/emotes/mouth.svg').then(r => r.text()).then(svg => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'mouth-overlay';
|
||||||
|
container.innerHTML = svg;
|
||||||
|
this.mouthSvg = container.firstElementChild;
|
||||||
|
const mouthPos = this.animations?.emote?.['mouth'] || {x: 50, y: 60, r: 0};
|
||||||
|
container.style.left = `${mouthPos.x}%`;
|
||||||
|
container.style.top = `${mouthPos.y}%`;
|
||||||
|
container.style.transform = `translate(-50%, -50%) rotate(${mouthPos.r || 0}deg)`;
|
||||||
|
container.style.width = '50px';
|
||||||
|
container.style.height = '25px';
|
||||||
|
const avatarContainer = this.shadowRoot.querySelector('.avatar-container');
|
||||||
|
if(avatarContainer) avatarContainer.appendChild(container);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMouthAnimation() {
|
||||||
|
const tts = TTS.getInstance();
|
||||||
|
let mouthAnimationInterval = null;
|
||||||
|
tts.on('onSentenceStart', () => {
|
||||||
|
if(mouthAnimationInterval) return;
|
||||||
|
const next = () => {
|
||||||
|
mouthAnimationInterval = setTimeout(() => {
|
||||||
|
next();
|
||||||
|
this.toggleMouthState();
|
||||||
|
}, ~~(Math.random() * 100) + 100);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
tts.on('onSentenceEnd', () => this.setMouthState('closed'));
|
||||||
|
tts.on('onComplete', () => {
|
||||||
|
if(mouthAnimationInterval) {
|
||||||
|
clearTimeout(mouthAnimationInterval);
|
||||||
|
mouthAnimationInterval = null;
|
||||||
|
}
|
||||||
|
this.setMouthState('closed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMouthState() {
|
||||||
|
if(!this.mouthSvg) return;
|
||||||
|
this.setMouthState(this.mouthState === 'open' ? 'partial' : 'open');
|
||||||
|
}
|
||||||
|
|
||||||
|
setMouthState(state) {
|
||||||
|
if(!this.mouthSvg) return;
|
||||||
|
this.mouthState = state;
|
||||||
|
this.mouthSvg.classList.remove('closed', 'partial', 'open');
|
||||||
|
this.mouthSvg.classList.add(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(all = true) {
|
||||||
|
if(all) {
|
||||||
|
const a = this.shadowRoot.querySelector('.avatar');
|
||||||
|
a.animate([{filter: 'drop-shadow(2px 4px 6px black) grayscale(0%) brightness(100%)'}], {duration: 100, fill: 'forwards'});
|
||||||
|
}
|
||||||
|
this.activeEmotes.forEach(e => e.remove());
|
||||||
|
this.activeEmotes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
emote(emote) {
|
||||||
|
const animate = (e, emote, style, index) => {
|
||||||
|
const duration = 3000;
|
||||||
|
if(emote === 'blush') {
|
||||||
|
e.animate([
|
||||||
|
{transform: `scale(0.75) rotate(${style.r ?? 0}deg)`, opacity: 0},
|
||||||
|
{transform: `scale(0.75) rotate(${style.r ?? 0}deg)`, opacity: 1}
|
||||||
|
], {duration: duration, easing: 'ease-out', fill: 'forwards'});
|
||||||
|
} else if(emote === 'cry') {
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(-25%) scale(0)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(0) scale(0.5)`}
|
||||||
|
], {duration: duration, easing: 'ease-out', fill: 'forwards'});
|
||||||
|
} else if(emote === 'dead') {
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(0)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(-10%)`}
|
||||||
|
], {duration: duration, easing: 'ease-out', fill: 'forwards'});
|
||||||
|
setTimeout(() => {
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(-10%)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(-8%)`}
|
||||||
|
], {duration: 1500, easing: 'ease-in-out', iterations: Infinity, direction: 'alternate'});
|
||||||
|
}, duration);
|
||||||
|
} else if(emote === 'drool') {
|
||||||
|
e.src = `${this.navi.api}/emotes/tear.png`;
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(-12.5%) scale(0)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(0) scale(0.25)`}
|
||||||
|
], {duration: duration, easing: 'ease-out', fill: 'forwards'});
|
||||||
|
} else if(emote === 'love') {
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(0.5)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(0.7)`}
|
||||||
|
], {duration: 200, easing: 'steps(2, jump-end)', iterations: Infinity, direction: 'alternate'});
|
||||||
|
} else if(emote === 'question') {
|
||||||
|
e.style.transform = `rotate(${style.r ?? 0}deg)`;
|
||||||
|
e.animate([
|
||||||
|
{opacity: 1, offset: 0},
|
||||||
|
{opacity: 1, offset: 0.49},
|
||||||
|
{opacity: 0, offset: 0.5},
|
||||||
|
{opacity: 0, offset: 1}
|
||||||
|
], {duration: 200, iterations: Infinity, direction: 'alternate', delay: (index % 2) * 200});
|
||||||
|
} else if(emote === 'realize') {
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(0.9)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(1.1)`}
|
||||||
|
], {duration: 500, easing: 'ease-out', iterations: Infinity, direction: 'alternate'});
|
||||||
|
} else if(emote === 'stars') {
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(0.25)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(0.3)`}
|
||||||
|
], {duration: 100, easing: 'steps(2, jump-end)', iterations: Infinity, direction: 'alternate'});
|
||||||
|
} else if(emote === 'stress') {
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(0.9)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(1.1)`}
|
||||||
|
], {duration: 333, easing: 'ease-out', iterations: Infinity, direction: 'alternate'});
|
||||||
|
} else if(emote === 'sigh') {
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) translate(0, 0)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) translate(10%, 10%)`}
|
||||||
|
], {duration: duration, easing: 'ease-out', fill: 'forwards'});
|
||||||
|
} else if(emote === 'sweat') {
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(0.5) translateY(0)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) scale(0.5) translateY(20%)`}
|
||||||
|
], {duration: duration, easing: 'ease-out', fill: 'forwards'});
|
||||||
|
} else if(emote === 'tear') {
|
||||||
|
e.animate([
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(-15%) scale(0)`},
|
||||||
|
{transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(0) scale(0.3)`}
|
||||||
|
], {duration: duration, easing: 'ease-out', fill: 'forwards'});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!emote || emote === 'none') return this.clear();
|
||||||
|
if(!this.animations.emote[emote]) throw new Error(`Invalid animation: ${emote}`);
|
||||||
|
const pos = this.animations.emote[emote];
|
||||||
|
this.clear(false);
|
||||||
|
|
||||||
|
const a = this.shadowRoot.querySelector('.avatar');
|
||||||
|
const container = this.shadowRoot.querySelector('.avatar-container');
|
||||||
|
const positions = Array.isArray(pos) ? pos : (pos.x != null ? [pos] : []);
|
||||||
|
|
||||||
|
if(['dead', 'grey'].includes(emote)) {
|
||||||
|
a.animate([
|
||||||
|
{filter: 'drop-shadow(2px 4px 6px black) grayscale(100%) brightness(150%)'}
|
||||||
|
], {duration: 100, fill: 'forwards'});
|
||||||
|
} else {
|
||||||
|
a.animate([
|
||||||
|
{filter: 'drop-shadow(2px 4px 6px black) grayscale(0%) brightness(100%)'}
|
||||||
|
], {duration: 100, fill: 'forwards'});
|
||||||
|
}
|
||||||
|
|
||||||
|
positions.forEach((p, i) => {
|
||||||
|
const e = document.createElement('img');
|
||||||
|
e.className = 'emote-overlay';
|
||||||
|
e.src = `${this.navi.api}/emotes/${emote}.png`;
|
||||||
|
e.style.top = `${p.y}%`;
|
||||||
|
e.style.left = `${p.x}%`;
|
||||||
|
container.appendChild(e);
|
||||||
|
this.activeEmotes.push(e);
|
||||||
|
animate(e, emote, p, i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('avatar-component', AvatarComponent);
|
||||||
185
public/components/btn.mjs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
function contrast(color) {
|
||||||
|
const exploded = color?.match(color.length >= 6 ? /[0-9a-fA-F]{2}/g : /[0-9a-fA-F]/g);
|
||||||
|
if(!exploded || exploded?.length < 3) return 'black';
|
||||||
|
const [r, g, b] = exploded.map(hex => parseInt(hex.length === 1 ? `${hex}${hex}` : hex, 16));
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||||
|
return luminance > 0.5 ? 'black' : 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
function shadeColor(hex, amount) {
|
||||||
|
function dec2Hex(num) {
|
||||||
|
const hex = Math.round(num * 255).toString(16);
|
||||||
|
return hex.length === 1 ? '0' + hex : hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hex2Int(hex) {
|
||||||
|
let r = 0,
|
||||||
|
g = 0,
|
||||||
|
b = 0;
|
||||||
|
if(hex.length === 4) {
|
||||||
|
r = parseInt(hex[1] + hex[1], 16);
|
||||||
|
g = parseInt(hex[2] + hex[2], 16);
|
||||||
|
b = parseInt(hex[3] + hex[3], 16);
|
||||||
|
} else {
|
||||||
|
r = parseInt(hex.slice(1, 3), 16);
|
||||||
|
g = parseInt(hex.slice(3, 5), 16);
|
||||||
|
b = parseInt(hex.slice(5, 7), 16);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
r,
|
||||||
|
g,
|
||||||
|
b
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hue2rgb(p, q, t) {
|
||||||
|
if(t < 0) t += 1;
|
||||||
|
if(t > 1) t -= 1;
|
||||||
|
if(t < 1 / 6) return p + (q - p) * 6 * t;
|
||||||
|
if(t < 1 / 2) return q;
|
||||||
|
if(t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function int2Hex(r, g, b) {
|
||||||
|
return '#' + dec2Hex(r) + dec2Hex(g) + dec2Hex(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
r,
|
||||||
|
g,
|
||||||
|
b
|
||||||
|
} = hex2Int(hex);
|
||||||
|
r /= 255;
|
||||||
|
g /= 255;
|
||||||
|
b /= 255;
|
||||||
|
const max = Math.max(r, g, b),
|
||||||
|
min = Math.min(r, g, b);
|
||||||
|
let h,
|
||||||
|
s,
|
||||||
|
l = (max + min) / 2;
|
||||||
|
|
||||||
|
if(max === min) {
|
||||||
|
h = s = 0;
|
||||||
|
} else {
|
||||||
|
const d = max - min;
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
switch(max) {
|
||||||
|
case r:
|
||||||
|
h = (g - b) / d + (g < b ? 6 : 0);
|
||||||
|
break;
|
||||||
|
case g:
|
||||||
|
h = (b - r) / d + 2;
|
||||||
|
break;
|
||||||
|
case b:
|
||||||
|
h = (r - g) / d + 4;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
h = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
l = Math.max(0, Math.min(1, l + amount));
|
||||||
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||||
|
const p = 2 * l - q;
|
||||||
|
return int2Hex(hue2rgb(p, q, h + 1 / 3), hue2rgb(p, q, h), hue2rgb(p, q, h - 1 / 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
class BtnComponent extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({mode: 'open'});
|
||||||
|
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
--base-color: #ccc;
|
||||||
|
--dark-color: #999;
|
||||||
|
--light-color: #eee;
|
||||||
|
--text-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 40px;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
transition: transform 0.1s, background-color 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
cursor: url('/assets/cursor.png'), auto;
|
||||||
|
fill: var(--text-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:not(.disabled) {
|
||||||
|
background: var(--base-color);
|
||||||
|
border: 2px solid var(--dark-color);
|
||||||
|
box-shadow: 0 4px 0 var(--dark-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:not(.disabled):hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background: var(--light-color);
|
||||||
|
border: 2px solid var(--base-color);
|
||||||
|
box-shadow: 0 6px 0 var(--base-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:not(.disabled):active {
|
||||||
|
transform: translateY(2px);
|
||||||
|
background: var(--base-color);
|
||||||
|
border: 2px solid var(--dark-color);
|
||||||
|
box-shadow: 0 2px 0 var(--dark-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.disabled {
|
||||||
|
cursor: no-drop;
|
||||||
|
background: var(--dark-color);
|
||||||
|
border: 2px solid var(--base-color);
|
||||||
|
box-shadow: 0 4px 0 var(--dark-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="btn">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.shadowRoot.querySelector('.btn').addEventListener('click', (e) => {
|
||||||
|
if(this.hasAttribute('disabled')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateColors();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['color', 'disabled'];
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
|
if(name === 'color') this.updateColors();
|
||||||
|
if(name === 'disabled') {
|
||||||
|
const disabled = this.hasAttribute('disabled');
|
||||||
|
this.shadowRoot.querySelector('.btn').classList[disabled ? 'add' : 'remove']('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColors() {
|
||||||
|
const hex = this.getAttribute('color');
|
||||||
|
this.shadowRoot.host.style.setProperty('--base-color', hex);
|
||||||
|
this.shadowRoot.host.style.setProperty('--dark-color', shadeColor(hex, -.1));
|
||||||
|
this.shadowRoot.host.style.setProperty('--light-color', shadeColor(hex, .1));
|
||||||
|
this.shadowRoot.host.style.setProperty('--text-color', contrast(hex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('btn-component', BtnComponent);
|
||||||
@@ -1,35 +1,28 @@
|
|||||||
|
import './btn.mjs';
|
||||||
|
|
||||||
class JukeboxComponent extends HTMLElement {
|
class JukeboxComponent extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.navi = window.navi;
|
||||||
// Use global singleton 🎵
|
this.navi.init();
|
||||||
this.api = window.netNaviAPI;
|
this.unsubscribeWorld = this.navi.on('world:data', (data) => {
|
||||||
|
console.log(data, this.navi.world.data);
|
||||||
|
this.render()
|
||||||
|
});
|
||||||
|
|
||||||
this.playlist = [];
|
this.playlist = [];
|
||||||
this.currentTrackIndex = 0;
|
this.currentTrackIndex = 0;
|
||||||
this.bgMusic = null;
|
this.bgMusic = null;
|
||||||
this.isMuted = false;
|
this.isMuted = false;
|
||||||
this.hasInteracted = false;
|
this.hasInteracted = false;
|
||||||
this.theme = null;
|
|
||||||
this.isPlaylistMode = false;
|
this.isPlaylistMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
this.render();
|
|
||||||
this.setupAPIListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
if(this.unsubscribeWorld) this.unsubscribeWorld();
|
if(this.unsubscribeWorld) this.unsubscribeWorld();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAPIListeners() {
|
|
||||||
this.unsubscribeWorld = this.api.on('world:loaded', (data) => {
|
|
||||||
if(data.theme?.music) this.loadMusic(data.theme.music, data.theme);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.shadowRoot.innerHTML = `
|
this.shadowRoot.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
@@ -44,8 +37,8 @@ class JukeboxComponent extends HTMLElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background: var(--dialogue-header-bg, #ffffff);
|
background: ${this.navi.world.data.theme.colors.primary};
|
||||||
border: 3px solid var(--dialogue-border, #000);
|
border: 3px solid ${this.navi.world.data.theme.colors.border};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@@ -55,8 +48,9 @@ class JukeboxComponent extends HTMLElement {
|
|||||||
.track-info {
|
.track-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--dialogue-bg, #8b5cf6);
|
background: ${this.navi.world.data.theme.colors.background};
|
||||||
color: var(--dialogue-text, #ffffff);
|
border: 2px solid ${this.navi.world.data.theme.colors.border};
|
||||||
|
color: ${this.navi.world.data.theme.colors.text};
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
@@ -64,6 +58,7 @@ class JukeboxComponent extends HTMLElement {
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
cursor: text;
|
||||||
}
|
}
|
||||||
.track-name {
|
.track-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -77,69 +72,37 @@ class JukeboxComponent extends HTMLElement {
|
|||||||
0% { transform: translateX(0); }
|
0% { transform: translateX(0); }
|
||||||
100% { transform: translateX(-100%); }
|
100% { transform: translateX(-100%); }
|
||||||
}
|
}
|
||||||
.controls-row {
|
.hidden {
|
||||||
display: flex;
|
display: none;
|
||||||
gap: 8px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
.control-btn {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
background: var(--button-bg, #6366f1);
|
|
||||||
border: 2px solid var(--dialogue-border, #000);
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 20px;
|
|
||||||
box-shadow: 0 3px 0 var(--button-shadow, #4338ca);
|
|
||||||
transition: transform 0.1s;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.control-btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 0 var(--button-shadow, #4338ca);
|
|
||||||
}
|
|
||||||
.control-btn:active {
|
|
||||||
transform: translateY(2px);
|
|
||||||
box-shadow: 0 1px 0 var(--button-shadow, #4338ca);
|
|
||||||
}
|
|
||||||
.control-btn svg {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
.hidden { display: none; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<button class="control-btn" id="simple-mute-btn">
|
<btn-component id="simple-mute-btn" color="${this.navi.world.data.theme.colors.accent}">
|
||||||
<svg viewBox="0 0 24 24">
|
<svg viewBox="0 0 24 24">
|
||||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</btn-component>
|
||||||
|
|
||||||
<div class="audio-controls hidden" id="playlist-controls">
|
<div class="audio-controls hidden" id="playlist-controls">
|
||||||
<div class="controls-row">
|
<btn-component id="prev-btn" color="${this.navi.world.data.theme.colors.accent}">
|
||||||
<button class="control-btn" id="prev-btn">
|
|
||||||
<svg viewBox="0 0 24 24">
|
<svg viewBox="0 0 24 24">
|
||||||
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
|
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</btn-component>
|
||||||
</div>
|
|
||||||
<div class="track-info">
|
<div class="track-info">
|
||||||
<span class="track-name" id="track-name">No track loaded</span>
|
<span class="track-name" id="track-name">No track loaded</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-row">
|
<div style="display: flex; gap: 8px">
|
||||||
<button class="control-btn" id="next-btn">
|
<btn-component id="next-btn" color="${this.navi.world.data.theme.colors.accent}">
|
||||||
<svg viewBox="0 0 24 24">
|
<svg viewBox="0 0 24 24">
|
||||||
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
|
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</btn-component>
|
||||||
<button class="control-btn" id="mute-btn">
|
<btn-component id="mute-btn" color="#c0392b">
|
||||||
<svg viewBox="0 0 24 24">
|
<svg viewBox="0 0 24 24">
|
||||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</btn-component>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -147,18 +110,15 @@ class JukeboxComponent extends HTMLElement {
|
|||||||
this.shadowRoot.getElementById('mute-btn').addEventListener('click', () => this.toggleMute());
|
this.shadowRoot.getElementById('mute-btn').addEventListener('click', () => this.toggleMute());
|
||||||
this.shadowRoot.getElementById('prev-btn').addEventListener('click', () => this.previousTrack());
|
this.shadowRoot.getElementById('prev-btn').addEventListener('click', () => this.previousTrack());
|
||||||
this.shadowRoot.getElementById('next-btn').addEventListener('click', () => this.nextTrack());
|
this.shadowRoot.getElementById('next-btn').addEventListener('click', () => this.nextTrack());
|
||||||
|
this.loadMusic(this.navi.world.data.theme.music);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMusic(musicConfig, theme) {
|
loadMusic(musicConfig) {
|
||||||
if(!musicConfig) return;
|
if(!musicConfig) return;
|
||||||
|
|
||||||
this.theme = theme;
|
|
||||||
this.isPlaylistMode = Array.isArray(musicConfig);
|
this.isPlaylistMode = Array.isArray(musicConfig);
|
||||||
this.playlist = Array.isArray(musicConfig) ? musicConfig : [musicConfig];
|
this.playlist = Array.isArray(musicConfig) ? musicConfig : [musicConfig];
|
||||||
this.currentTrackIndex = 0;
|
this.currentTrackIndex = 0;
|
||||||
|
|
||||||
this.applyThemeColors();
|
|
||||||
|
|
||||||
if (this.isPlaylistMode) {
|
if (this.isPlaylistMode) {
|
||||||
this.shadowRoot.getElementById('simple-mute-btn').classList.add('hidden');
|
this.shadowRoot.getElementById('simple-mute-btn').classList.add('hidden');
|
||||||
this.shadowRoot.getElementById('playlist-controls').classList.remove('hidden');
|
this.shadowRoot.getElementById('playlist-controls').classList.remove('hidden');
|
||||||
@@ -205,15 +165,6 @@ class JukeboxComponent extends HTMLElement {
|
|||||||
trackName.textContent = `[${trackNum}] ${fileName}`;
|
trackName.textContent = `[${trackNum}] ${fileName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyThemeColors() {
|
|
||||||
if (!this.theme) return;
|
|
||||||
const root = this.shadowRoot.host.style;
|
|
||||||
Object.entries(this.theme.colors).forEach(([key, value]) => {
|
|
||||||
const cssVar = '--' + key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
||||||
root.setProperty(cssVar, value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupAutoplayHandler() {
|
setupAutoplayHandler() {
|
||||||
const startMusic = () => {
|
const startMusic = () => {
|
||||||
if (!this.hasInteracted && !this.isMuted && this.bgMusic) {
|
if (!this.hasInteracted && !this.isMuted && this.bgMusic) {
|
||||||
977
public/components/llm.mjs
Normal file
@@ -0,0 +1,977 @@
|
|||||||
|
import './btn.mjs';
|
||||||
|
import {TTS} from '../tts.mjs';
|
||||||
|
|
||||||
|
class LlmComponent extends HTMLElement {
|
||||||
|
hideTools = ['emote', 'personalize', 'recall', 'remember']
|
||||||
|
|
||||||
|
get isOpen() { return this.isDialogueOpen; };
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.navi = window.navi;
|
||||||
|
this.navi.init().then(() => this.render());
|
||||||
|
|
||||||
|
this.isReceiving = false;
|
||||||
|
this.streamComplete = false;
|
||||||
|
this.isDialogueOpen = false;
|
||||||
|
this.isExpanded = false;
|
||||||
|
this.messageHistory = [];
|
||||||
|
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
this.streamBuffer = '';
|
||||||
|
this.typingIndex = 0;
|
||||||
|
this.typingInterval = null;
|
||||||
|
this.currentRequest = null;
|
||||||
|
this.attachedFiles = [];
|
||||||
|
this.currentStreamingMessage = null;
|
||||||
|
|
||||||
|
// TTS setup
|
||||||
|
this.tts = null;
|
||||||
|
this.streamingSpeech = null;
|
||||||
|
this.autoSpeak = false;
|
||||||
|
this.speakingMessageIdx = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<style>
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: ${this.navi.theme.primary};
|
||||||
|
border: 2px solid ${this.navi.theme.border};
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialogue-box {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 600px;
|
||||||
|
max-width: 90vw;
|
||||||
|
height: 60%;
|
||||||
|
transition: width 0.3s ease-out, height 0.3s ease-out, max-width 0.3s ease-out, left 0.3s ease-out, transform 0.3s ease-out;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialogue-box.minimized {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialogue-box.expanded {
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogue-content {
|
||||||
|
background: ${this.navi.theme.background};
|
||||||
|
border: 5px solid ${this.navi.theme.border};
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0 -4px 20px rgba(0,0,0,0.5);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 600px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialogue-box.expanded .dialogue-content {
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogue-header {
|
||||||
|
user-select: none;
|
||||||
|
padding: 10px 10px 10px 15px;
|
||||||
|
background: ${this.navi.theme.primary};
|
||||||
|
border-bottom: 3px solid ${this.navi.theme.border};
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialogue-box.expanded .dialogue-header {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-body {
|
||||||
|
padding: 1.75rem 1.25rem;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.75rem;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialogue-box.expanded .message-body {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper.user {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper.assistant {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: ${this.navi.theme.text};
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
opacity: 0.7;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
word-wrap: break-word;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper.user .message-bubble {
|
||||||
|
background: ${this.navi.theme.primary};
|
||||||
|
color: ${this.navi.theme.primaryContrast};
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid ${this.navi.theme.border};
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper.navi .message-bubble {
|
||||||
|
color: ${this.navi.theme.text};
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0 8px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-action-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: ${this.navi.theme.text};
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-action-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: ${this.navi.theme.text};
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
text-shadow:
|
||||||
|
3px 3px 0 rgba(74, 144, 226, 0.3),
|
||||||
|
-1px -1px 0 rgba(0,0,0,0.2);
|
||||||
|
letter-spacing: 3px;
|
||||||
|
animation: glowPulse 2s ease-in-out infinite;
|
||||||
|
background: linear-gradient(45deg, transparent 30%, rgba(74, 144, 226, 0.1) 50%, transparent 70%);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: shimmer 3s ease-in-out infinite;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glowPulse {
|
||||||
|
0%, 100% { opacity: 0.8; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 16px;
|
||||||
|
background: currentColor;
|
||||||
|
margin-left: 2px;
|
||||||
|
animation: blink 0.5s infinite;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 49% { opacity: 1; }
|
||||||
|
50%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call {
|
||||||
|
display: inline-block;
|
||||||
|
background: ${this.navi.theme.primary};
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin: 2px;
|
||||||
|
border: 2px solid #000;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
animation: toolPulse 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toolPulse {
|
||||||
|
0%, 100% { transform: scale(1); opacity: 1; }
|
||||||
|
50% { transform: scale(1.05); opacity: 0.9; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: ${this.navi.theme.primary};
|
||||||
|
color: ${this.navi.theme.primaryContrast};
|
||||||
|
padding: 4px 10px;
|
||||||
|
margin: 0;
|
||||||
|
border: 2px solid ${this.navi.theme.border};
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-files {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper.user .message-files {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper.navi .message-files {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attached-files {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-top: 3px solid ${this.navi.theme.border};
|
||||||
|
background: ${this.navi.theme.backgroundDark};
|
||||||
|
display: none;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attached-files.has-files {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attached-file {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: ${this.navi.theme.primary};
|
||||||
|
color: ${this.navi.theme.primaryContrast};
|
||||||
|
border: 2px solid ${this.navi.theme.border};
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attached-file .file-name {
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attached-file .remove-file {
|
||||||
|
background: #e74c3c;
|
||||||
|
border: 1px solid black;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attached-file .remove-file:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogue-input {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 3px solid ${this.navi.theme.border};
|
||||||
|
background: ${this.navi.theme.backgroundDark};
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogue-input textarea {
|
||||||
|
width: 100%;
|
||||||
|
background: ${this.navi.theme.background};
|
||||||
|
border: 3px solid ${this.navi.theme.border};
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: ${this.navi.theme.text};
|
||||||
|
resize: none;
|
||||||
|
min-height: 44px;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogue-input textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogue-input textarea:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn, .attach-btn, .dialogue-send-btn {
|
||||||
|
background: ${this.navi.theme.accent};
|
||||||
|
border: 3px solid ${this.navi.theme.border};
|
||||||
|
color: ${this.navi.theme.accentContrast};
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 3px 0 ${this.navi.theme.accentDark};
|
||||||
|
transition: background 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogue-send-btn.stop {
|
||||||
|
background: #e74c3c;
|
||||||
|
box-shadow: 0 1px 0 #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogue-send-btn.stop:active {
|
||||||
|
box-shadow: 0 1px 0 #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-fill-even {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="dialogue-box" class="minimized">
|
||||||
|
<div class="dialogue-content">
|
||||||
|
<div class="dialogue-header" id="dialogue-header">
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.5rem; flex-grow: 1;">
|
||||||
|
<img alt="logo" src="${this.navi.icon}" style="height: 32px; width: auto;">
|
||||||
|
<span style="color: ${this.navi.theme.primaryContrast}; font-size: 1.75rem">${this.navi.info.name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-buttons">
|
||||||
|
<btn-component id="autospeak-btn" color="${this.navi.theme.accent}">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
|
||||||
|
</svg>
|
||||||
|
</btn-component>
|
||||||
|
<btn-component id="expand-btn" color="${this.navi.theme.accent}">
|
||||||
|
<svg class="expand-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="3" y="3" width="8" height="8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
<rect x="13" y="13" width="8" height="8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
<path d="M11 3 L11 11 L3 11" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
<path d="M13 21 L13 13 L21 13" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</btn-component>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="message-body" id="message-body">
|
||||||
|
<div class="empty-state">NetNavi v1.0.0</div>
|
||||||
|
</div>
|
||||||
|
<div class="attached-files" id="attached-files"></div>
|
||||||
|
<div class="dialogue-input">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<textarea id="dialogue-input" placeholder="Type your message..." rows="1"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="button-row">
|
||||||
|
<btn-component class="flex-fill-even" id="clear-btn" color="#c0392b">CLEAR</btn-component>
|
||||||
|
<btn-component class="flex-fill-even" id="attach-btn" color="${this.navi.theme.accent}">ATTACH</btn-component>
|
||||||
|
<btn-component class="flex-fill-even" id="dialogue-send" color="${this.navi.theme.accent}">SEND</btn-component>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="file-input" multiple accept="*/*">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Init TTS
|
||||||
|
this.tts = TTS.getInstance();
|
||||||
|
|
||||||
|
const dialogueHeader = this.shadowRoot.getElementById('dialogue-header');
|
||||||
|
const dialogueInput = this.shadowRoot.getElementById('dialogue-input');
|
||||||
|
const dialogueSend = this.shadowRoot.getElementById('dialogue-send');
|
||||||
|
const attachBtn = this.shadowRoot.getElementById('attach-btn');
|
||||||
|
const fileInput = this.shadowRoot.getElementById('file-input');
|
||||||
|
const clearBtn = this.shadowRoot.getElementById('clear-btn');
|
||||||
|
const expandBtn = this.shadowRoot.getElementById('expand-btn');
|
||||||
|
const autospeakBtn = this.shadowRoot.getElementById('autospeak-btn');
|
||||||
|
|
||||||
|
dialogueInput.addEventListener('input', () => {
|
||||||
|
dialogueInput.style.height = 'auto';
|
||||||
|
dialogueInput.style.height = Math.min(dialogueInput.scrollHeight, 120) + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogueInput.addEventListener('paste', (e) => {
|
||||||
|
const text = e.clipboardData.getData('text');
|
||||||
|
if (text.length > 1000) {
|
||||||
|
e.preventDefault();
|
||||||
|
const blob = new Blob([text], { type: 'text/plain' });
|
||||||
|
const file = new File([blob], 'pasted_text.txt', { type: 'text/plain' });
|
||||||
|
this.addFile(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogueHeader.addEventListener('click', (e) => {
|
||||||
|
if (e.target === dialogueHeader || e.target.closest('.header-buttons')) return;
|
||||||
|
this.toggleDialogue();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogueInput.addEventListener('focus', () => {
|
||||||
|
if(!this.isDialogueOpen) this.openDialogue();
|
||||||
|
});
|
||||||
|
|
||||||
|
clearBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.clearChat();
|
||||||
|
});
|
||||||
|
|
||||||
|
expandBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggleExpand();
|
||||||
|
});
|
||||||
|
|
||||||
|
autospeakBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggleAutoSpeak();
|
||||||
|
});
|
||||||
|
|
||||||
|
attachBtn.addEventListener('click', () => fileInput.click());
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
Array.from(e.target.files).forEach(file => this.addFile(file));
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogueSend.addEventListener('click', () => {
|
||||||
|
const buttonText = dialogueSend.textContent;
|
||||||
|
if (buttonText === 'SKIP') this.skipToEnd();
|
||||||
|
else if (buttonText === 'STOP') this.abortStream();
|
||||||
|
else this.sendMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogueInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey && !this.isReceiving) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAutoSpeak() {
|
||||||
|
this.autoSpeak = !this.autoSpeak;
|
||||||
|
const btn = this.shadowRoot.getElementById('autospeak-btn');
|
||||||
|
|
||||||
|
const mutedSVG = '<svg viewBox="0 0 24 24"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>';
|
||||||
|
const unmutedSVG = '<svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>';
|
||||||
|
|
||||||
|
btn.innerHTML = this.autoSpeak ? unmutedSVG : mutedSVG;
|
||||||
|
|
||||||
|
if (!this.autoSpeak && this.tts) {
|
||||||
|
this.tts.stop();
|
||||||
|
this.streamingSpeech = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSpeech(idx) {
|
||||||
|
if (!this.tts) return;
|
||||||
|
|
||||||
|
if (this.speakingMessageIdx === idx) {
|
||||||
|
this.tts.stop();
|
||||||
|
this.speakingMessageIdx = null;
|
||||||
|
this.updateMessageActions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tts.stop();
|
||||||
|
const message = this.messageHistory[idx];
|
||||||
|
if (!message || message.isUser || !message.text) return;
|
||||||
|
|
||||||
|
this.speakingMessageIdx = idx;
|
||||||
|
this.updateMessageActions();
|
||||||
|
|
||||||
|
this.tts.speak(message.text).then(() => {
|
||||||
|
this.speakingMessageIdx = null;
|
||||||
|
this.updateMessageActions();
|
||||||
|
}).catch(() => {
|
||||||
|
this.speakingMessageIdx = null;
|
||||||
|
this.updateMessageActions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMessageActions() {
|
||||||
|
this.messageHistory.forEach((msg, idx) => {
|
||||||
|
if (msg.isUser || !msg.element) return;
|
||||||
|
|
||||||
|
let actionsDiv = msg.element.querySelector('.message-actions');
|
||||||
|
if (!actionsDiv) {
|
||||||
|
actionsDiv = document.createElement('div');
|
||||||
|
actionsDiv.className = 'message-actions';
|
||||||
|
msg.element.appendChild(actionsDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSpeaking = this.speakingMessageIdx === idx;
|
||||||
|
actionsDiv.innerHTML = `
|
||||||
|
<button class="message-action-btn" data-action="speak" data-idx="${idx}">
|
||||||
|
<i class="fa ${isSpeaking ? 'fa-stop' : 'fa-volume-up'}"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const speakBtn = actionsDiv.querySelector('[data-action="speak"]');
|
||||||
|
speakBtn.addEventListener('click', () => this.toggleSpeech(idx));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearChat() {
|
||||||
|
this.messageHistory = [];
|
||||||
|
const messageBody = this.shadowRoot.getElementById('message-body');
|
||||||
|
messageBody.innerHTML = '<div class="empty-state">NetNavi v1.0.0</div>';
|
||||||
|
this.navi.clearChat();
|
||||||
|
if (this.tts) this.tts.stop();
|
||||||
|
this.speakingMessageIdx = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleExpand() {
|
||||||
|
this.isExpanded = !this.isExpanded;
|
||||||
|
const dialogueBox = this.shadowRoot.getElementById('dialogue-box');
|
||||||
|
const expandBtn = this.shadowRoot.getElementById('expand-btn');
|
||||||
|
|
||||||
|
dialogueBox.classList.toggle('expanded', this.isExpanded);
|
||||||
|
|
||||||
|
expandBtn.innerHTML = this.isExpanded ? `
|
||||||
|
<svg class="expand-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="8" y="8" width="8" height="8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
<path d="M8 8 L3 3 M8 3 L8 8 L3 8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
<path d="M16 16 L21 21 M16 21 L16 16 L21 16" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
` : `
|
||||||
|
<svg class="expand-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="3" y="3" width="8" height="8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
<rect x="13" y="13" width="8" height="8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
<path d="M11 3 L11 11 L3 11" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
<path d="M13 21 L13 13 L21 13" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile(file) {
|
||||||
|
this.attachedFiles.push(file);
|
||||||
|
this.renderAttachedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFile(index) {
|
||||||
|
this.attachedFiles.splice(index, 1);
|
||||||
|
this.renderAttachedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAttachedFiles() {
|
||||||
|
const container = this.shadowRoot.getElementById('attached-files');
|
||||||
|
|
||||||
|
if (this.attachedFiles.length === 0) {
|
||||||
|
container.classList.remove('has-files');
|
||||||
|
container.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.classList.add('has-files');
|
||||||
|
container.innerHTML = this.attachedFiles.map((file, i) => `
|
||||||
|
<div class="attached-file">
|
||||||
|
<span class="file-name" title="${file.name}">${file.name}</span>
|
||||||
|
<button class="remove-file" data-index="${i}">✕</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
container.querySelectorAll('.remove-file').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
this.removeFile(parseInt(btn.dataset.index));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fileToString(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader[file.type.startsWith('text/') || file.name.endsWith('.txt') ? 'readAsText' : 'readAsDataURL'](file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTime(date) {
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDialogue() {
|
||||||
|
this.isDialogueOpen = !this.isDialogueOpen;
|
||||||
|
const dialogueBox = this.shadowRoot.getElementById('dialogue-box');
|
||||||
|
dialogueBox.classList.toggle('minimized');
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('dialogue-toggle', {
|
||||||
|
detail: { isOpen: this.isDialogueOpen }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
openDialogue() {
|
||||||
|
this.isDialogueOpen = true;
|
||||||
|
const dialogueBox = this.shadowRoot.getElementById('dialogue-box');
|
||||||
|
dialogueBox.classList.remove('minimized');
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('dialogue-toggle', {
|
||||||
|
detail: { isOpen: this.isDialogueOpen }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
playTextBeep() {
|
||||||
|
const oscillator = this.audioCtx.createOscillator();
|
||||||
|
const gainNode = this.audioCtx.createGain();
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(this.audioCtx.destination);
|
||||||
|
oscillator.type = 'square';
|
||||||
|
oscillator.frequency.setValueAtTime(1200, this.audioCtx.currentTime);
|
||||||
|
gainNode.gain.setValueAtTime(0.1, this.audioCtx.currentTime);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 0.05);
|
||||||
|
oscillator.start(this.audioCtx.currentTime);
|
||||||
|
oscillator.stop(this.audioCtx.currentTime + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldAutoScroll() {
|
||||||
|
const messageBody = this.shadowRoot.getElementById('message-body');
|
||||||
|
const scrollThreshold = 50;
|
||||||
|
const distanceFromBottom = messageBody.scrollHeight - messageBody.scrollTop - messageBody.clientHeight;
|
||||||
|
return distanceFromBottom <= scrollThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
const messageBody = this.shadowRoot.getElementById('message-body');
|
||||||
|
messageBody.scrollTop = messageBody.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(text, isUser) {
|
||||||
|
const messageBody = this.shadowRoot.getElementById('message-body');
|
||||||
|
const emptyState = messageBody.querySelector('.empty-state');
|
||||||
|
if (emptyState) messageBody.innerHTML = '';
|
||||||
|
|
||||||
|
// Extract file badges and clean text
|
||||||
|
const fileBadges = [];
|
||||||
|
const fileRegex = /<file name="([^"]+)">[\s\S]*?<\/file>/g;
|
||||||
|
let match;
|
||||||
|
while ((match = fileRegex.exec(text)) !== null) {
|
||||||
|
fileBadges.push(match[1]);
|
||||||
|
}
|
||||||
|
const cleanText = text.replace(fileRegex, '').trim();
|
||||||
|
|
||||||
|
const messageWrapper = document.createElement('div');
|
||||||
|
messageWrapper.className = `message-wrapper ${isUser ? 'user' : 'navi'}`;
|
||||||
|
|
||||||
|
const fileBadgesHtml = fileBadges.length > 0
|
||||||
|
? `<div class="message-files">${fileBadges.map(name =>
|
||||||
|
`<span class="file-badge">📄 ${name}</span>`).join('')}</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
messageWrapper.innerHTML = `
|
||||||
|
${fileBadgesHtml}
|
||||||
|
<div class="message-bubble">${cleanText}</div>`;
|
||||||
|
|
||||||
|
messageBody.appendChild(messageWrapper);
|
||||||
|
const msgData = { text, html: cleanText, isUser, element: messageWrapper, timestamp: Date.now() };
|
||||||
|
this.messageHistory.push(msgData);
|
||||||
|
|
||||||
|
if (!isUser) {
|
||||||
|
this.updateMessageActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
startStreaming() {
|
||||||
|
this.isReceiving = true;
|
||||||
|
this.streamComplete = false;
|
||||||
|
this.streamBuffer = '';
|
||||||
|
this.typingIndex = 0;
|
||||||
|
|
||||||
|
const dialogueSend = this.shadowRoot.getElementById('dialogue-send');
|
||||||
|
const attachBtn = this.shadowRoot.getElementById('attach-btn');
|
||||||
|
const clearBtn = this.shadowRoot.getElementById('clear-btn');
|
||||||
|
const messageBody = this.shadowRoot.getElementById('message-body');
|
||||||
|
|
||||||
|
attachBtn.disabled = true;
|
||||||
|
clearBtn.disabled = true;
|
||||||
|
dialogueSend.textContent = 'STOP';
|
||||||
|
dialogueSend.setAttribute('color', '#c0392b');
|
||||||
|
attachBtn.setAttribute('disabled', true);
|
||||||
|
clearBtn.setAttribute('disabled', true);
|
||||||
|
|
||||||
|
const emptyState = messageBody.querySelector('.empty-state');
|
||||||
|
if (emptyState) messageBody.innerHTML = '';
|
||||||
|
|
||||||
|
const messageWrapper = document.createElement('div');
|
||||||
|
messageWrapper.className = 'message-wrapper navi';
|
||||||
|
messageWrapper.innerHTML = `<div class="message-bubble" id="streaming-bubble"></div>`;
|
||||||
|
|
||||||
|
messageBody.appendChild(messageWrapper);
|
||||||
|
|
||||||
|
this.currentStreamingMessage = { text: '', html: '', isUser: false, element: messageWrapper, timestamp: Date.now() };
|
||||||
|
this.messageHistory.push(this.currentStreamingMessage);
|
||||||
|
|
||||||
|
// Start TTS streaming if autoSpeak is on
|
||||||
|
if (this.autoSpeak && this.tts) {
|
||||||
|
this.streamingSpeech = this.tts.speakStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollToBottom();
|
||||||
|
|
||||||
|
this.typingInterval = setInterval(() => this.typeNextChar(), 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStreamChunk(chunk) {
|
||||||
|
if (!this.isReceiving) this.startStreaming();
|
||||||
|
|
||||||
|
if (chunk.text) {
|
||||||
|
this.streamBuffer += chunk.text;
|
||||||
|
// Feed to TTS stream
|
||||||
|
if (this.streamingSpeech) {
|
||||||
|
this.streamingSpeech.next(chunk.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chunk.tool && !this.hideTools.includes(chunk.tool)) this.streamBuffer += `<span class="tool-call">⚡ ${chunk.tool}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStreamComplete(response) {
|
||||||
|
this.streamComplete = true;
|
||||||
|
if (this.typingIndex >= this.streamBuffer.length) {
|
||||||
|
await this.cleanupStreaming();
|
||||||
|
} else {
|
||||||
|
const dialogueSend = this.shadowRoot.getElementById('dialogue-send');
|
||||||
|
dialogueSend.textContent = 'SKIP';
|
||||||
|
dialogueSend.setAttribute('color', '#f39c12');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typeNextChar() {
|
||||||
|
if (this.typingIndex >= this.streamBuffer.length && this.streamComplete) {
|
||||||
|
this.cleanupStreaming();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.typingIndex >= this.streamBuffer.length) return;
|
||||||
|
|
||||||
|
const bubble = this.shadowRoot.getElementById('streaming-bubble');
|
||||||
|
if (!bubble) return;
|
||||||
|
|
||||||
|
const shouldScroll = this.shouldAutoScroll();
|
||||||
|
|
||||||
|
if (this.streamBuffer[this.typingIndex] === '<') {
|
||||||
|
const tagEnd = this.streamBuffer.indexOf('>', this.typingIndex);
|
||||||
|
if (tagEnd !== -1) {
|
||||||
|
const tag = this.streamBuffer.substring(this.typingIndex, tagEnd + 1);
|
||||||
|
this.currentStreamingMessage.html += tag;
|
||||||
|
this.currentStreamingMessage.text += tag;
|
||||||
|
this.typingIndex = tagEnd + 1;
|
||||||
|
bubble.innerHTML = this.currentStreamingMessage.html + '<span class="text-cursor"></span>';
|
||||||
|
if (shouldScroll) this.scrollToBottom();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char = this.streamBuffer[this.typingIndex];
|
||||||
|
this.currentStreamingMessage.text += char;
|
||||||
|
this.currentStreamingMessage.html += char;
|
||||||
|
|
||||||
|
bubble.innerHTML = this.currentStreamingMessage.html + '<span class="text-cursor"></span>';
|
||||||
|
|
||||||
|
if (char !== ' ' && char !== '<') {
|
||||||
|
this.playTextBeep();
|
||||||
|
if ('vibrate' in navigator) navigator.vibrate(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.typingIndex++;
|
||||||
|
if (shouldScroll) this.scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
async skipToEnd() {
|
||||||
|
clearInterval(this.typingInterval);
|
||||||
|
|
||||||
|
const bubble = this.shadowRoot.getElementById('streaming-bubble');
|
||||||
|
|
||||||
|
this.currentStreamingMessage.text = this.streamBuffer;
|
||||||
|
this.currentStreamingMessage.html = this.streamBuffer;
|
||||||
|
this.typingIndex = this.streamBuffer.length;
|
||||||
|
|
||||||
|
if (bubble) bubble.innerHTML = this.currentStreamingMessage.html;
|
||||||
|
|
||||||
|
this.scrollToBottom();
|
||||||
|
await this.cleanupStreaming();
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanupStreaming() {
|
||||||
|
clearInterval(this.typingInterval);
|
||||||
|
|
||||||
|
// Finalize TTS stream
|
||||||
|
if (this.streamingSpeech) {
|
||||||
|
await this.streamingSpeech.done();
|
||||||
|
this.streamingSpeech = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isReceiving = false;
|
||||||
|
this.streamComplete = false;
|
||||||
|
|
||||||
|
const dialogueSend = this.shadowRoot.getElementById('dialogue-send');
|
||||||
|
const attachBtn = this.shadowRoot.getElementById('attach-btn');
|
||||||
|
const clearBtn = this.shadowRoot.getElementById('clear-btn');
|
||||||
|
const bubble = this.shadowRoot.getElementById('streaming-bubble');
|
||||||
|
|
||||||
|
attachBtn.disabled = false;
|
||||||
|
clearBtn.disabled = false;
|
||||||
|
dialogueSend.textContent = 'SEND';
|
||||||
|
dialogueSend.setAttribute('color', this.navi.theme.accent);
|
||||||
|
attachBtn.removeAttribute('disabled');
|
||||||
|
clearBtn.removeAttribute('disabled');
|
||||||
|
|
||||||
|
if (bubble) {
|
||||||
|
bubble.id = '';
|
||||||
|
bubble.innerHTML = this.currentStreamingMessage.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateMessageActions();
|
||||||
|
|
||||||
|
this.streamBuffer = '';
|
||||||
|
this.typingIndex = 0;
|
||||||
|
this.currentRequest = null;
|
||||||
|
this.currentStreamingMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
abortStream() {
|
||||||
|
if (this.currentRequest?.abort) {
|
||||||
|
this.currentRequest.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(this.typingInterval);
|
||||||
|
|
||||||
|
if (this.currentStreamingMessage) {
|
||||||
|
this.streamBuffer = this.currentStreamingMessage.text || '';
|
||||||
|
this.typingIndex = this.streamBuffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tts) this.tts.stop();
|
||||||
|
this.streamingSpeech = null;
|
||||||
|
|
||||||
|
this.cleanupStreaming();
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage() {
|
||||||
|
const dialogueInput = this.shadowRoot.getElementById('dialogue-input');
|
||||||
|
let text = dialogueInput.value.trim();
|
||||||
|
if ((!text && this.attachedFiles.length === 0) || this.isReceiving) return;
|
||||||
|
|
||||||
|
if (this.attachedFiles.length > 0) {
|
||||||
|
const fileBlocks = await Promise.all(
|
||||||
|
this.attachedFiles.map(async (file) => {
|
||||||
|
const content = await this.fileToString(file);
|
||||||
|
return `<file name="${file.name}">${content}</file>`;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
text = text + '\n\n' + fileBlocks.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogueInput.value = '';
|
||||||
|
dialogueInput.style.height = 'auto';
|
||||||
|
|
||||||
|
this.addMessage(text, true);
|
||||||
|
|
||||||
|
this.attachedFiles = [];
|
||||||
|
this.renderAttachedFiles();
|
||||||
|
|
||||||
|
// Send via API with streaming callback 💬
|
||||||
|
this.currentRequest = this.navi.ask(text, (chunk) => this.handleStreamChunk(chunk));
|
||||||
|
|
||||||
|
// Handle completion/errors with promise
|
||||||
|
try {
|
||||||
|
const response = await this.currentRequest;
|
||||||
|
await this.handleStreamComplete(response);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message !== 'Aborted by user') {
|
||||||
|
console.error('❌ LLM Error:', error);
|
||||||
|
this.addMessage(`Error: ${error.message || 'Something went wrong'}`, false);
|
||||||
|
}
|
||||||
|
await this.cleanupStreaming();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('llm-component', LlmComponent);
|
||||||
380
public/components/world.mjs
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
// ============================================
|
||||||
|
// CONSTANTS
|
||||||
|
// ============================================
|
||||||
|
const TILE_WIDTH = 64;
|
||||||
|
const TILE_HEIGHT = 32;
|
||||||
|
const TILE_DEPTH = 16;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// THEME HANDLER
|
||||||
|
// ============================================
|
||||||
|
function applyTheme(theme) {
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
|
if(theme.background.image) {
|
||||||
|
body.style.backgroundImage = `url(${theme.background.image})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.style.backgroundSize = theme.background.style || 'cover';
|
||||||
|
body.style.backgroundPosition = 'center';
|
||||||
|
body.style.backgroundRepeat = 'no-repeat';
|
||||||
|
body.style.backgroundAttachment = 'fixed';
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
Object.entries(theme.colors).forEach(([key, value]) => {
|
||||||
|
const cssVar = `--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
||||||
|
root.style.setProperty(cssVar, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🎨 Theme applied:', theme.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TILE RENDERER
|
||||||
|
// ============================================
|
||||||
|
function isoToScreen(gridX, gridY) {
|
||||||
|
return {
|
||||||
|
x: (gridX - gridY) * (TILE_WIDTH / 2) + window.innerWidth / 2,
|
||||||
|
y: (gridX + gridY) * (TILE_HEIGHT / 2) + 100
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTile(tileData, theme) {
|
||||||
|
const graphics = new PIXI.Graphics();
|
||||||
|
const pos = isoToScreen(tileData.x, tileData.y);
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
top: parseInt(theme.colors.tileTop.replace('#', '0x')),
|
||||||
|
side: parseInt(theme.colors.tileSide.replace('#', '0x')),
|
||||||
|
grid: parseInt(theme.colors.gridColor.replace('#', '0x')),
|
||||||
|
highlight: parseInt(theme.colors.tileHighlight.replace('#', '0x')),
|
||||||
|
gridHighlight: parseInt(theme.colors.gridHighlight.replace('#', '0x'))
|
||||||
|
};
|
||||||
|
|
||||||
|
function drawNormalTile() {
|
||||||
|
graphics.clear();
|
||||||
|
graphics.beginFill(colors.top);
|
||||||
|
graphics.lineStyle(1, colors.grid);
|
||||||
|
graphics.moveTo(pos.x, pos.y);
|
||||||
|
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
||||||
|
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.lineTo(pos.x, pos.y);
|
||||||
|
graphics.endFill();
|
||||||
|
|
||||||
|
graphics.beginFill(colors.side);
|
||||||
|
graphics.lineStyle(1, colors.grid);
|
||||||
|
graphics.moveTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
||||||
|
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
||||||
|
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH);
|
||||||
|
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.endFill();
|
||||||
|
|
||||||
|
graphics.beginFill(colors.side);
|
||||||
|
graphics.lineStyle(1, colors.grid);
|
||||||
|
graphics.moveTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
||||||
|
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
||||||
|
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH);
|
||||||
|
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.endFill();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawHighlightTile() {
|
||||||
|
graphics.clear();
|
||||||
|
graphics.beginFill(colors.highlight);
|
||||||
|
graphics.lineStyle(2, colors.gridHighlight);
|
||||||
|
graphics.moveTo(pos.x, pos.y);
|
||||||
|
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
||||||
|
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.lineTo(pos.x, pos.y);
|
||||||
|
graphics.endFill();
|
||||||
|
|
||||||
|
graphics.beginFill(colors.side);
|
||||||
|
graphics.lineStyle(1, colors.gridHighlight);
|
||||||
|
graphics.moveTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
||||||
|
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
||||||
|
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH);
|
||||||
|
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.endFill();
|
||||||
|
|
||||||
|
graphics.beginFill(colors.side);
|
||||||
|
graphics.lineStyle(1, colors.gridHighlight);
|
||||||
|
graphics.moveTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
||||||
|
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
||||||
|
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH);
|
||||||
|
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
|
graphics.endFill();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawNormalTile();
|
||||||
|
|
||||||
|
graphics.interactive = true;
|
||||||
|
graphics.buttonMode = true;
|
||||||
|
graphics.gridX = tileData.x;
|
||||||
|
graphics.gridY = tileData.y;
|
||||||
|
|
||||||
|
graphics.on('pointerover', () => {
|
||||||
|
drawHighlightTile();
|
||||||
|
});
|
||||||
|
|
||||||
|
graphics.on('pointerout', () => {
|
||||||
|
drawNormalTile();
|
||||||
|
});
|
||||||
|
|
||||||
|
return graphics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPet(gridX, gridY, name = 'PET') {
|
||||||
|
const container = new PIXI.Container();
|
||||||
|
const pos = isoToScreen(gridX, gridY);
|
||||||
|
|
||||||
|
const body = new PIXI.Graphics();
|
||||||
|
body.beginFill(0xff6b9d);
|
||||||
|
body.drawCircle(0, -30, 15);
|
||||||
|
body.endFill();
|
||||||
|
|
||||||
|
body.beginFill(0xffffff);
|
||||||
|
body.drawCircle(-5, -32, 4);
|
||||||
|
body.drawCircle(5, -32, 4);
|
||||||
|
body.endFill();
|
||||||
|
|
||||||
|
body.beginFill(0x000000);
|
||||||
|
body.drawCircle(-5, -32, 2);
|
||||||
|
body.drawCircle(5, -32, 2);
|
||||||
|
body.endFill();
|
||||||
|
|
||||||
|
const nameText = new PIXI.Text(name, {
|
||||||
|
fontFamily: 'Courier New',
|
||||||
|
fontSize: 12,
|
||||||
|
fill: '#ffffff',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 2
|
||||||
|
});
|
||||||
|
nameText.anchor.set(0.5);
|
||||||
|
nameText.y = -50;
|
||||||
|
|
||||||
|
container.addChild(body);
|
||||||
|
container.addChild(nameText);
|
||||||
|
container.x = pos.x;
|
||||||
|
container.y = pos.y;
|
||||||
|
container.gridX = gridX;
|
||||||
|
container.gridY = gridY;
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// GAME CLASS
|
||||||
|
// ============================================
|
||||||
|
class Game {
|
||||||
|
constructor() {
|
||||||
|
this.worldId = '';
|
||||||
|
this.world = null;
|
||||||
|
this.app = null;
|
||||||
|
this.pet = null;
|
||||||
|
this.otherPlayers = new Map();
|
||||||
|
this.isMoving = false;
|
||||||
|
this.dialogue = null;
|
||||||
|
this.keys = {};
|
||||||
|
|
||||||
|
// Use global singleton 🌍
|
||||||
|
this.navi = window.navi;
|
||||||
|
this.worldActions = null;
|
||||||
|
|
||||||
|
this.playerInfo = {
|
||||||
|
name: 'Guest',
|
||||||
|
apiUrl: this.navi.navi
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
// Join world with callbacks 🌍
|
||||||
|
await this.navi.init();
|
||||||
|
this.worldActions = this.navi.connect(this.worldId);
|
||||||
|
|
||||||
|
this.worldActions.onData = (data) => {
|
||||||
|
this.world = data;
|
||||||
|
applyTheme(this.world.theme);
|
||||||
|
this.initializeRenderer();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.worldActions.onPlayers = (players) => {
|
||||||
|
players.forEach(player => {
|
||||||
|
if(player.name !== this.navi.info.name) {
|
||||||
|
this.addOtherPlayer(player);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.worldActions.onJoined = (player) => {
|
||||||
|
this.addOtherPlayer(player);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.worldActions.onMoved = (data) => {
|
||||||
|
const sprite = this.otherPlayers.get(data.socketId);
|
||||||
|
if(sprite) {
|
||||||
|
this.moveOtherPlayer(sprite, data.x, data.y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.worldActions.onLeft = (data) => {
|
||||||
|
const sprite = this.otherPlayers.get(data.socketId);
|
||||||
|
if(sprite) this.otherPlayers.delete(data.socketId);
|
||||||
|
};
|
||||||
|
this.worldActions.onError = (error) => console.error('❌ World error:', error);
|
||||||
|
|
||||||
|
console.log('✨ Game initializing...');
|
||||||
|
} catch(error) {
|
||||||
|
console.error('❌ Failed to initialize game:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addOtherPlayer(player) {
|
||||||
|
const sprite = createPet(player.x, player.y, player.name);
|
||||||
|
sprite.alpha = 0.7;
|
||||||
|
this.otherPlayers.set(player.socketId, sprite);
|
||||||
|
this.app.stage.addChild(sprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveOtherPlayer(sprite, targetX, targetY) {
|
||||||
|
const targetPos = isoToScreen(targetX, targetY);
|
||||||
|
|
||||||
|
const startX = sprite.x;
|
||||||
|
const startY = sprite.y;
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
progress += 0.08;
|
||||||
|
if(progress >= 1) {
|
||||||
|
sprite.x = targetPos.x;
|
||||||
|
sprite.y = targetPos.y;
|
||||||
|
sprite.gridX = targetX;
|
||||||
|
sprite.gridY = targetY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite.x = startX + (targetPos.x - startX) * progress;
|
||||||
|
sprite.y = startY + (targetPos.y - startY) * progress;
|
||||||
|
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeRenderer() {
|
||||||
|
this.app = new PIXI.Application({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
backgroundAlpha: 0,
|
||||||
|
antialias: true,
|
||||||
|
resolution: window.devicePixelRatio || 1,
|
||||||
|
autoDensity: true
|
||||||
|
});
|
||||||
|
document.getElementById('game').appendChild(this.app.view);
|
||||||
|
|
||||||
|
const tiles = new PIXI.Container();
|
||||||
|
this.app.stage.addChild(tiles);
|
||||||
|
|
||||||
|
this.world.tiles.forEach(tileData => {
|
||||||
|
const tile = createTile(tileData, this.world.theme);
|
||||||
|
tile.on('pointerdown', () => this.movePetTo(tile.gridX, tile.gridY));
|
||||||
|
tiles.addChild(tile);
|
||||||
|
});
|
||||||
|
|
||||||
|
const spawn = this.world.tiles.find(t => t.type === 'spawn');
|
||||||
|
this.pet = createPet(spawn.x, spawn.y, this.playerInfo.name);
|
||||||
|
this.app.stage.addChild(this.pet);
|
||||||
|
|
||||||
|
this.dialogue = document.getElementById('llm');
|
||||||
|
|
||||||
|
this.setupInput();
|
||||||
|
this.app.ticker.add(() => this.gameLoop());
|
||||||
|
}
|
||||||
|
|
||||||
|
movePetTo(targetX, targetY) {
|
||||||
|
if(this.isMoving || targetX < 0 || targetX >= this.world.gridSize || targetY < 0 || targetY >= this.world.gridSize) return;
|
||||||
|
|
||||||
|
this.isMoving = true;
|
||||||
|
const targetPos = isoToScreen(targetX, targetY);
|
||||||
|
|
||||||
|
const startX = this.pet.x;
|
||||||
|
const startY = this.pet.y;
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
progress += 0.08;
|
||||||
|
if(progress >= 1) {
|
||||||
|
this.pet.x = targetPos.x;
|
||||||
|
this.pet.y = targetPos.y;
|
||||||
|
this.pet.gridX = targetX;
|
||||||
|
this.pet.gridY = targetY;
|
||||||
|
this.isMoving = false;
|
||||||
|
|
||||||
|
// Use API action to send move 📤
|
||||||
|
if(this.worldActions) {
|
||||||
|
this.worldActions.move(targetX, targetY);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pet.x = startX + (targetPos.x - startX) * progress;
|
||||||
|
this.pet.y = startY + (targetPos.y - startY) * progress;
|
||||||
|
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupInput() {
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if(this.dialogue.isOpen) return;
|
||||||
|
|
||||||
|
this.keys[e.key.toLowerCase()] = true;
|
||||||
|
|
||||||
|
if(!this.isMoving) {
|
||||||
|
let newX = this.pet.gridX;
|
||||||
|
let newY = this.pet.gridY;
|
||||||
|
|
||||||
|
if(this.keys['w'] || this.keys['arrowup']) {
|
||||||
|
newY--;
|
||||||
|
} else if(this.keys['s'] || this.keys['arrowdown']) {
|
||||||
|
newY++;
|
||||||
|
} else if(this.keys['a'] || this.keys['arrowleft']) {
|
||||||
|
newX--;
|
||||||
|
} else if(this.keys['d'] || this.keys['arrowright']) {
|
||||||
|
newX++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.movePetTo(newX, newY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('keyup', (e) => {
|
||||||
|
this.keys[e.key.toLowerCase()] = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
gameLoop() {
|
||||||
|
if(!this.isMoving && this.pet) {
|
||||||
|
this.pet.children[0].y = -30 + Math.sin(Date.now() / 300) * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// START GAME
|
||||||
|
// ============================================
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const worldId = urlParams.get('world');
|
||||||
|
|
||||||
|
const game = new Game();
|
||||||
|
game.worldId = worldId;
|
||||||
|
game.init();
|
||||||
BIN
public/emotes/blush.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/emotes/cry.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/emotes/dead.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/emotes/love.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
30
public/emotes/mouth.svg
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 50" class="mouth closed">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.mouth-path {
|
||||||
|
stroke: #000;
|
||||||
|
stroke-width: 2;
|
||||||
|
transition: d 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Closed state - slight natural curve smile */
|
||||||
|
.mouth.closed .mouth-path {
|
||||||
|
d: path("M 20 25 Q 50 30 80 25");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Partial open - opens downward */
|
||||||
|
.mouth.partial .mouth-path {
|
||||||
|
fill: #ff6b9d;
|
||||||
|
d: path("M 20 25 Q 50 35 80 25");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Open - wider downward opening */
|
||||||
|
.mouth.open .mouth-path {
|
||||||
|
fill: #ff6b9d;
|
||||||
|
d: path("M 20 25 Q 50 42 80 25");
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<path class="mouth-path" d="M 20 25 Q 50 28 80 25"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 669 B |
BIN
public/emotes/question.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/emotes/realize.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
public/emotes/sigh.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
public/emotes/stress.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
public/emotes/sweat.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/emotes/tear.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
@@ -1,21 +1,34 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>NetNavi v1.0.0</title>
|
<title>NetNavi</title>
|
||||||
<link rel="icon" href="/favicon.png"/>
|
<link rel="icon" href="/favicon.png"/>
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<meta name="theme-color" content="{{THEME_PRIMARY}}">
|
||||||
|
<meta property="og:title" content="NetNavi">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="NetNavi">
|
||||||
|
<meta property="og:site_name" content="NetNavi">
|
||||||
|
<meta name="description" content="Network Navigation Program">
|
||||||
|
<meta property="og:description" content="Network Navigation Program">
|
||||||
|
<meta property="og:image" content="/banner?size=1200x630">
|
||||||
|
<meta property="og:logo" content="/favicon?size=180">
|
||||||
|
<meta name="apple-touch-icon" content="/favicon.png">
|
||||||
|
<meta name="apple-touch-startup-image" content="/favicon.png">
|
||||||
|
<meta property="og:url" content="{{PUBLIC_URL}}">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
|
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
|
||||||
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
|
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/7.3.2/pixi.min.js"></script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
box-sizing: border-box !important;
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: url('/assets/cursor.png'), auto !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
@@ -23,10 +36,12 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input {
|
*, button, input {
|
||||||
cursor: default;
|
cursor: url('/assets/cursor.png'), auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -34,26 +49,110 @@
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
background: black;
|
||||||
|
transition: 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
.digital-background {
|
||||||
display: block;
|
position: fixed;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scanline {
|
||||||
|
0% { transform: translateY(-100%); }
|
||||||
|
50% { transform: translateY(100%); }
|
||||||
|
100% { transform: translateY(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanline {
|
||||||
|
animation: scanline 11s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes up-down {
|
||||||
|
0% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-50px); }
|
||||||
|
100% { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.up-down {
|
||||||
|
animation: up-down 31s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes left-right {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
50% { transform: translateX(-50px); }
|
||||||
|
100% { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-right {
|
||||||
|
animation: left-right 37s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#avatar {
|
||||||
|
position:fixed;
|
||||||
|
height: 110%;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
left:0;
|
||||||
|
bottom: 0;
|
||||||
|
transform: translateY(20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
left: 50%;
|
||||||
|
bottom: 50%;
|
||||||
|
transform: translate(-50%, 50%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="game"></div>
|
<svg class="digital-background" xmlns="http://www.w3.org/2000/svg">
|
||||||
<jukebox-component id="jukebox"></jukebox-component>
|
<defs>
|
||||||
|
<pattern id="grid" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="#ffffffaa" stroke-width="0.5"/>
|
||||||
|
</pattern>
|
||||||
|
<pattern id="gridL" width="200" height="200" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 200 0 L 0 0 0 200" fill="none" stroke="#ffffffaa" stroke-width="1"/>
|
||||||
|
</pattern>
|
||||||
|
<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#0a0a0a;stop-opacity:0.7" />
|
||||||
|
<stop offset="100%" style="stop-color:#1a1a2e;stop-opacity:0.7" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background gradient -->
|
||||||
|
<rect width="100%" height="100%" fill="url(#bg-gradient)"/>
|
||||||
|
<!-- Digital grid -->
|
||||||
|
<rect class="up-down" width="100%" height="120%" fill="url(#grid)"/>
|
||||||
|
<rect class="left-right" width="120%" height="100%" fill="url(#gridL)"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<avatar-component id="avatar"></avatar-component>
|
||||||
|
|
||||||
|
<svg class="digital-background" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Scanline effect -->
|
||||||
|
<rect class="scanline" width="100%" height="8" fill="#ffffff22"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
<llm-component id="llm"></llm-component>
|
<llm-component id="llm"></llm-component>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import Navi from './navi.mjs';
|
||||||
|
|
||||||
<script src="/netnavi-api.js"></script>
|
const navi = window.navi = new Navi();
|
||||||
<script>window.netNaviAPI = new NetNaviAPI();</script>
|
navi.init().then(async () => {
|
||||||
<script src="/jukebox.js"></script>
|
document.body.style.background = navi.theme.accent;
|
||||||
<script src="/llm.js"></script>
|
});
|
||||||
<script src="/world.js"></script>
|
</script>
|
||||||
|
<script type="module" src="/components/avatar.mjs"></script>
|
||||||
|
<script type="module" src="/components/llm.mjs"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
934
public/llm.js
@@ -1,934 +0,0 @@
|
|||||||
class LlmComponent extends HTMLElement {
|
|
||||||
hideTools = ['adjust_personality', 'recall', 'remember']
|
|
||||||
|
|
||||||
get isOpen() { return this.isDialogueOpen; };
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.attachShadow({ mode: 'open' });
|
|
||||||
|
|
||||||
// Use global singleton 🔥
|
|
||||||
this.api = window.netNaviAPI;
|
|
||||||
|
|
||||||
this.isTyping = false;
|
|
||||||
this.isReceiving = false;
|
|
||||||
this.streamComplete = false;
|
|
||||||
this.isDialogueOpen = false;
|
|
||||||
this.isExpanded = false;
|
|
||||||
this.messageHistory = [];
|
|
||||||
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
||||||
this.streamBuffer = '';
|
|
||||||
this.typingIndex = 0;
|
|
||||||
this.typingInterval = null;
|
|
||||||
this.currentRequest = null;
|
|
||||||
this.attachedFiles = [];
|
|
||||||
this.currentStreamingMessage = null;
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
this.initEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.shadowRoot.innerHTML = `
|
|
||||||
<style>
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--dialogue-header-bg, #fff);
|
|
||||||
border: 2px solid var(--dialog-border, #000);
|
|
||||||
border-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dialogue-box {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 600px;
|
|
||||||
max-width: 90vw;
|
|
||||||
height: 600px;
|
|
||||||
transition: width 0.3s ease-out, height 0.3s ease-out, max-width 0.3s ease-out, left 0.3s ease-out, transform 0.3s ease-out;
|
|
||||||
transform-origin: bottom center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dialogue-box.minimized {
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dialogue-box.expanded {
|
|
||||||
width: 100vw;
|
|
||||||
max-width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-content {
|
|
||||||
background: var(--dialogue-bg, #fff);
|
|
||||||
border: 5px solid var(--dialogue-border, #000);
|
|
||||||
border-radius: 16px 16px 0 0;
|
|
||||||
padding: 0;
|
|
||||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.5);
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-height: 600px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dialogue-box.expanded .dialogue-content {
|
|
||||||
max-height: 100vh;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-header {
|
|
||||||
user-select: none;
|
|
||||||
padding: 12px 20px;
|
|
||||||
background: var(--dialogue-header-bg, #fff);
|
|
||||||
border-bottom: 3px solid var(--dialogue-border, #000);
|
|
||||||
border-radius: 12px 12px 0 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dialogue-box.expanded .dialogue-header {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-name {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--dialogue-text, #000);
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-btn {
|
|
||||||
background: var(--button-bg, #4a90e2);
|
|
||||||
border: 2px solid var(--dialogue-border, #000);
|
|
||||||
color: var(--button-text, #fff);
|
|
||||||
padding: 8px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 3px 0 var(--button-shadow, #2a5a9a);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-btn:active {
|
|
||||||
transform: translateY(2px);
|
|
||||||
box-shadow: 0 1px 0 var(--button-shadow, #2a5a9a);
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-body {
|
|
||||||
padding: 20px;
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
min-height: 200px;
|
|
||||||
max-height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dialogue-box.expanded .message-body {
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-wrapper.user {
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-wrapper.assistant {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--dialogue-text, #000);
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
opacity: 0.7;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-bubble {
|
|
||||||
max-width: 80%;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
word-wrap: break-word;
|
|
||||||
border: 2px solid var(--dialogue-border, #000);
|
|
||||||
box-shadow: 2px 2px 0 rgba(0,0,0,0.2);
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-wrapper.user .message-bubble {
|
|
||||||
background: var(--button-bg, #4a90e2);
|
|
||||||
color: var(--button-text, #fff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-wrapper.assistant .message-bubble {
|
|
||||||
background: var(--dialogue-bg, #fff);
|
|
||||||
color: var(--dialogue-text, #000);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-timestamp {
|
|
||||||
font-size: 10px;
|
|
||||||
opacity: 0.6;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
color: var(--dialogue-text, #000);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--dialogue-text, #000);
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
text-shadow:
|
|
||||||
3px 3px 0 rgba(74, 144, 226, 0.3),
|
|
||||||
-1px -1px 0 rgba(0,0,0,0.2);
|
|
||||||
letter-spacing: 3px;
|
|
||||||
animation: glowPulse 2s ease-in-out infinite;
|
|
||||||
background: linear-gradient(45deg, transparent 30%, rgba(74, 144, 226, 0.1) 50%, transparent 70%);
|
|
||||||
background-size: 200% 200%;
|
|
||||||
animation: shimmer 3s ease-in-out infinite;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% { background-position: 0% 50%; }
|
|
||||||
50% { background-position: 100% 50%; }
|
|
||||||
100% { background-position: 0% 50%; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glowPulse {
|
|
||||||
0%, 100% { opacity: 0.8; }
|
|
||||||
50% { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-cursor {
|
|
||||||
display: inline-block;
|
|
||||||
width: 8px;
|
|
||||||
height: 16px;
|
|
||||||
background: currentColor;
|
|
||||||
margin-left: 2px;
|
|
||||||
animation: blink 0.5s infinite;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
|
||||||
0%, 49% { opacity: 1; }
|
|
||||||
50%, 100% { opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-call {
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--button-bg, #000);
|
|
||||||
color: #fff;
|
|
||||||
padding: 2px 8px;
|
|
||||||
margin: 2px;
|
|
||||||
border: 2px solid #000;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
animation: toolPulse 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes toolPulse {
|
|
||||||
0%, 100% { transform: scale(1); opacity: 1; }
|
|
||||||
50% { transform: scale(1.05); opacity: 0.9; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-badge {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--button-bg, #4a90e2);
|
|
||||||
color: var(--button-text, #fff);
|
|
||||||
padding: 4px 10px;
|
|
||||||
margin: 0;
|
|
||||||
border: 2px solid var(--dialogue-border, #000);
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-files {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 0 8px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-wrapper.user .message-files {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-wrapper.assistant .message-files {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attached-files {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-top: 3px solid var(--dialogue-border, #000);
|
|
||||||
background: var(--dialogue-input-bg, #f0f0f0);
|
|
||||||
display: none;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attached-files.has-files {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attached-file {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--dialogue-bg, #fff);
|
|
||||||
border: 2px solid var(--dialogue-border, #000);
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attached-file .file-name {
|
|
||||||
max-width: 150px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attached-file .remove-file {
|
|
||||||
background: #e74c3c;
|
|
||||||
border: none;
|
|
||||||
color: #fff;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attached-file .remove-file:hover {
|
|
||||||
background: #c0392b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-input {
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-top: 3px solid var(--dialogue-border, #000);
|
|
||||||
background: var(--dialogue-input-bg, #f0f0f0);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-wrapper {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-input textarea {
|
|
||||||
width: 100%;
|
|
||||||
background: var(--dialogue-bg, #fff);
|
|
||||||
border: 3px solid var(--dialogue-border, #000);
|
|
||||||
padding: 10px 14px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--dialogue-text, #000);
|
|
||||||
resize: none;
|
|
||||||
min-height: 44px;
|
|
||||||
max-height: 120px;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-input textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 2px var(--button-bg, #4a90e2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-input textarea:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn, .attach-btn, .dialogue-send-btn {
|
|
||||||
background: var(--button-bg, #4a90e2);
|
|
||||||
border: 3px solid var(--dialogue-border, #000);
|
|
||||||
color: var(--button-text, #fff);
|
|
||||||
padding: 10px 18px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 3px 0 var(--button-shadow, #2a5a9a);
|
|
||||||
transition: background 0.2s;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn {
|
|
||||||
background: #e74c3c;
|
|
||||||
box-shadow: 0 3px 0 #c0392b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn:active {
|
|
||||||
transform: translateY(2px);
|
|
||||||
box-shadow: 0 1px 0 #c0392b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attach-btn:active, .dialogue-send-btn:active {
|
|
||||||
transform: translateY(2px);
|
|
||||||
box-shadow: 0 1px 0 var(--button-shadow, #2a5a9a);
|
|
||||||
}
|
|
||||||
|
|
||||||
.attach-btn:disabled, .dialogue-send-btn:disabled, .clear-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-send-btn.stop {
|
|
||||||
background: #e74c3c;
|
|
||||||
box-shadow: 0 3px 0 #c0392b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-send-btn.stop:active {
|
|
||||||
box-shadow: 0 1px 0 #c0392b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-send-btn.skip {
|
|
||||||
background: #f39c12;
|
|
||||||
box-shadow: 0 3px 0 #d68910;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-send-btn.skip:active {
|
|
||||||
box-shadow: 0 1px 0 #d68910;
|
|
||||||
}
|
|
||||||
|
|
||||||
#file-input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div id="dialogue-box" class="minimized">
|
|
||||||
<div class="dialogue-content">
|
|
||||||
<div class="dialogue-header" id="dialogue-header">
|
|
||||||
<img alt="logo" src="/favicon.png" style="height: 32px; width: auto;">
|
|
||||||
<button class="expand-btn" id="expand-btn">
|
|
||||||
<svg class="expand-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="3" y="3" width="8" height="8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
<rect x="13" y="13" width="8" height="8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
<path d="M11 3 L11 11 L3 11" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
<path d="M13 21 L13 13 L21 13" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="message-body" id="message-body">
|
|
||||||
<div class="empty-state">NetNavi v1.0.0</div>
|
|
||||||
</div>
|
|
||||||
<div class="attached-files" id="attached-files"></div>
|
|
||||||
<div class="dialogue-input">
|
|
||||||
<div class="input-wrapper">
|
|
||||||
<textarea id="dialogue-input" placeholder="Type your message..." rows="1"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="button-row">
|
|
||||||
<button class="clear-btn" id="clear-btn">CLEAR</button>
|
|
||||||
<button class="attach-btn" id="attach-btn">ATTACH</button>
|
|
||||||
<button class="dialogue-send-btn" id="dialogue-send">SEND</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="file" id="file-input" multiple accept="*/*">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
initEventListeners() {
|
|
||||||
const dialogueBox = this.shadowRoot.getElementById('dialogue-box');
|
|
||||||
const dialogueHeader = this.shadowRoot.getElementById('dialogue-header');
|
|
||||||
const dialogueInput = this.shadowRoot.getElementById('dialogue-input');
|
|
||||||
const dialogueSend = this.shadowRoot.getElementById('dialogue-send');
|
|
||||||
const attachBtn = this.shadowRoot.getElementById('attach-btn');
|
|
||||||
const fileInput = this.shadowRoot.getElementById('file-input');
|
|
||||||
const clearBtn = this.shadowRoot.getElementById('clear-btn');
|
|
||||||
const expandBtn = this.shadowRoot.getElementById('expand-btn');
|
|
||||||
|
|
||||||
dialogueInput.addEventListener('input', () => {
|
|
||||||
dialogueInput.style.height = 'auto';
|
|
||||||
dialogueInput.style.height = Math.min(dialogueInput.scrollHeight, 120) + 'px';
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogueInput.addEventListener('paste', (e) => {
|
|
||||||
const text = e.clipboardData.getData('text');
|
|
||||||
if (text.length > 1000) {
|
|
||||||
e.preventDefault();
|
|
||||||
const blob = new Blob([text], { type: 'text/plain' });
|
|
||||||
const file = new File([blob], 'pasted_text.txt', { type: 'text/plain' });
|
|
||||||
this.addFile(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogueHeader.addEventListener('click', (e) => {
|
|
||||||
if (e.target === dialogueHeader || e.target.classList.contains('speaker-name')) {
|
|
||||||
this.toggleDialogue();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogueInput.addEventListener('focus', () => {
|
|
||||||
if (!this.isDialogueOpen) this.openDialogue();
|
|
||||||
});
|
|
||||||
|
|
||||||
clearBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.clearChat();
|
|
||||||
});
|
|
||||||
|
|
||||||
expandBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.toggleExpand();
|
|
||||||
});
|
|
||||||
|
|
||||||
attachBtn.addEventListener('click', () => fileInput.click());
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', (e) => {
|
|
||||||
Array.from(e.target.files).forEach(file => this.addFile(file));
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogueSend.addEventListener('click', () => {
|
|
||||||
const buttonText = dialogueSend.textContent;
|
|
||||||
if (buttonText === 'SKIP') this.skipToEnd();
|
|
||||||
else if (buttonText === 'STOP') this.abortStream();
|
|
||||||
else this.sendMessage();
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogueInput.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey && !this.isReceiving) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.sendMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
clearChat() {
|
|
||||||
this.messageHistory = [];
|
|
||||||
const messageBody = this.shadowRoot.getElementById('message-body');
|
|
||||||
messageBody.innerHTML = '<div class="empty-state">NetNavi v1.0.0</div>';
|
|
||||||
this.api.clearLLMHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleExpand() {
|
|
||||||
this.isExpanded = !this.isExpanded;
|
|
||||||
const dialogueBox = this.shadowRoot.getElementById('dialogue-box');
|
|
||||||
const expandBtn = this.shadowRoot.getElementById('expand-btn');
|
|
||||||
|
|
||||||
dialogueBox.classList.toggle('expanded', this.isExpanded);
|
|
||||||
|
|
||||||
expandBtn.innerHTML = this.isExpanded ? `
|
|
||||||
<svg class="expand-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="8" y="8" width="8" height="8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
<path d="M8 8 L3 3 M8 3 L8 8 L3 8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
<path d="M16 16 L21 21 M16 21 L16 16 L21 16" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
</svg>
|
|
||||||
` : `
|
|
||||||
<svg class="expand-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="3" y="3" width="8" height="8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
<rect x="13" y="13" width="8" height="8" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
<path d="M11 3 L11 11 L3 11" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
<path d="M13 21 L13 13 L21 13" stroke="currentColor" stroke-width="2.5" fill="none"/>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
addFile(file) {
|
|
||||||
this.attachedFiles.push(file);
|
|
||||||
this.renderAttachedFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFile(index) {
|
|
||||||
this.attachedFiles.splice(index, 1);
|
|
||||||
this.renderAttachedFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAttachedFiles() {
|
|
||||||
const container = this.shadowRoot.getElementById('attached-files');
|
|
||||||
|
|
||||||
if (this.attachedFiles.length === 0) {
|
|
||||||
container.classList.remove('has-files');
|
|
||||||
container.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.classList.add('has-files');
|
|
||||||
container.innerHTML = this.attachedFiles.map((file, i) => `
|
|
||||||
<div class="attached-file">
|
|
||||||
<span class="file-name" title="${file.name}">📄 ${file.name}</span>
|
|
||||||
<button class="remove-file" data-index="${i}">✕</button>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
container.querySelectorAll('.remove-file').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
this.removeFile(parseInt(btn.dataset.index));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async fileToString(file) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => resolve(reader.result);
|
|
||||||
reader.onerror = reject;
|
|
||||||
reader[file.type.startsWith('text/') || file.name.endsWith('.txt') ? 'readAsText' : 'readAsDataURL'](file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
processMessageForDisplay(text) {
|
|
||||||
return text.replace(/<file name="([^"]+)">[\s\S]*?<\/file>/g,
|
|
||||||
'<span class="file-badge">📄 $1</span>');
|
|
||||||
}
|
|
||||||
|
|
||||||
formatTime(date) {
|
|
||||||
const hours = date.getHours().toString().padStart(2, '0');
|
|
||||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
||||||
return `${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDialogue() {
|
|
||||||
this.isDialogueOpen = !this.isDialogueOpen;
|
|
||||||
const dialogueBox = this.shadowRoot.getElementById('dialogue-box');
|
|
||||||
dialogueBox.classList.toggle('minimized');
|
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent('dialogue-toggle', {
|
|
||||||
detail: { isOpen: this.isDialogueOpen }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
openDialogue() {
|
|
||||||
this.isDialogueOpen = true;
|
|
||||||
const dialogueBox = this.shadowRoot.getElementById('dialogue-box');
|
|
||||||
dialogueBox.classList.remove('minimized');
|
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent('dialogue-toggle', {
|
|
||||||
detail: { isOpen: this.isDialogueOpen }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
closeDialogue() {
|
|
||||||
this.isDialogueOpen = false;
|
|
||||||
const dialogueBox = this.shadowRoot.getElementById('dialogue-box');
|
|
||||||
dialogueBox.classList.add('minimized');
|
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent('dialogue-toggle', {
|
|
||||||
detail: { isOpen: this.isDialogueOpen }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
playTextBeep() {
|
|
||||||
const oscillator = this.audioCtx.createOscillator();
|
|
||||||
const gainNode = this.audioCtx.createGain();
|
|
||||||
oscillator.connect(gainNode);
|
|
||||||
gainNode.connect(this.audioCtx.destination);
|
|
||||||
oscillator.type = 'square';
|
|
||||||
oscillator.frequency.setValueAtTime(1200, this.audioCtx.currentTime);
|
|
||||||
gainNode.gain.setValueAtTime(0.1, this.audioCtx.currentTime);
|
|
||||||
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 0.05);
|
|
||||||
oscillator.start(this.audioCtx.currentTime);
|
|
||||||
oscillator.stop(this.audioCtx.currentTime + 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldAutoScroll() {
|
|
||||||
const messageBody = this.shadowRoot.getElementById('message-body');
|
|
||||||
const scrollThreshold = 50;
|
|
||||||
const distanceFromBottom = messageBody.scrollHeight - messageBody.scrollTop - messageBody.clientHeight;
|
|
||||||
return distanceFromBottom <= scrollThreshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToBottom() {
|
|
||||||
const messageBody = this.shadowRoot.getElementById('message-body');
|
|
||||||
messageBody.scrollTop = messageBody.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
addMessage(text, isUser) {
|
|
||||||
const messageBody = this.shadowRoot.getElementById('message-body');
|
|
||||||
const emptyState = messageBody.querySelector('.empty-state');
|
|
||||||
if (emptyState) messageBody.innerHTML = '';
|
|
||||||
|
|
||||||
// Extract file badges and clean text
|
|
||||||
const fileBadges = [];
|
|
||||||
const fileRegex = /<file name="([^"]+)">[\s\S]*?<\/file>/g;
|
|
||||||
let match;
|
|
||||||
while ((match = fileRegex.exec(text)) !== null) {
|
|
||||||
fileBadges.push(match[1]);
|
|
||||||
}
|
|
||||||
const cleanText = text.replace(fileRegex, '').trim();
|
|
||||||
|
|
||||||
const messageWrapper = document.createElement('div');
|
|
||||||
messageWrapper.className = `message-wrapper ${isUser ? 'user' : 'assistant'}`;
|
|
||||||
const timestamp = this.formatTime(new Date());
|
|
||||||
|
|
||||||
const fileBadgesHtml = fileBadges.length > 0
|
|
||||||
? `<div class="message-files">${fileBadges.map(name =>
|
|
||||||
`<span class="file-badge">📄 ${name}</span>`).join('')}</div>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
messageWrapper.innerHTML = `
|
|
||||||
<div class="message-label">${isUser ? 'You' : 'PET'}</div>
|
|
||||||
${fileBadgesHtml}
|
|
||||||
<div class="message-bubble">${cleanText}</div>
|
|
||||||
<div class="message-timestamp">${timestamp}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
messageBody.appendChild(messageWrapper);
|
|
||||||
this.messageHistory.push({ text, html: cleanText, isUser, element: messageWrapper, timestamp });
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
startStreaming() {
|
|
||||||
this.isReceiving = true;
|
|
||||||
this.streamComplete = false;
|
|
||||||
this.streamBuffer = '';
|
|
||||||
this.typingIndex = 0;
|
|
||||||
|
|
||||||
const dialogueInput = this.shadowRoot.getElementById('dialogue-input');
|
|
||||||
const dialogueSend = this.shadowRoot.getElementById('dialogue-send');
|
|
||||||
const attachBtn = this.shadowRoot.getElementById('attach-btn');
|
|
||||||
const clearBtn = this.shadowRoot.getElementById('clear-btn');
|
|
||||||
const messageBody = this.shadowRoot.getElementById('message-body');
|
|
||||||
|
|
||||||
dialogueInput.disabled = true;
|
|
||||||
attachBtn.disabled = true;
|
|
||||||
clearBtn.disabled = true;
|
|
||||||
dialogueSend.textContent = 'STOP';
|
|
||||||
dialogueSend.classList.add('stop');
|
|
||||||
dialogueSend.classList.remove('skip');
|
|
||||||
|
|
||||||
const emptyState = messageBody.querySelector('.empty-state');
|
|
||||||
if (emptyState) messageBody.innerHTML = '';
|
|
||||||
|
|
||||||
const timestamp = this.formatTime(new Date());
|
|
||||||
const messageWrapper = document.createElement('div');
|
|
||||||
messageWrapper.className = 'message-wrapper assistant';
|
|
||||||
messageWrapper.innerHTML = `
|
|
||||||
<div class="message-label">PET</div>
|
|
||||||
<div class="message-bubble" id="streaming-bubble"></div>
|
|
||||||
<div class="message-timestamp">${timestamp}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
messageBody.appendChild(messageWrapper);
|
|
||||||
|
|
||||||
this.currentStreamingMessage = { text: '', html: '', isUser: false, element: messageWrapper, timestamp };
|
|
||||||
this.messageHistory.push(this.currentStreamingMessage);
|
|
||||||
|
|
||||||
this.scrollToBottom();
|
|
||||||
|
|
||||||
this.typingInterval = setInterval(() => this.typeNextChar(), 30);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleStreamChunk(chunk) {
|
|
||||||
if (!this.isReceiving) this.startStreaming();
|
|
||||||
|
|
||||||
if (chunk.text) this.streamBuffer += chunk.text;
|
|
||||||
if (chunk.tool && !this.hideTools.includes(chunk.tool)) this.streamBuffer += `<span class="tool-call">⚡ ${chunk.tool}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleStreamComplete(response) {
|
|
||||||
this.streamComplete = true;
|
|
||||||
|
|
||||||
if (this.typingIndex >= this.streamBuffer.length) {
|
|
||||||
this.cleanupStreaming();
|
|
||||||
} else {
|
|
||||||
const dialogueSend = this.shadowRoot.getElementById('dialogue-send');
|
|
||||||
dialogueSend.textContent = 'SKIP';
|
|
||||||
dialogueSend.classList.remove('stop');
|
|
||||||
dialogueSend.classList.add('skip');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typeNextChar() {
|
|
||||||
if (this.typingIndex >= this.streamBuffer.length && this.streamComplete) {
|
|
||||||
this.cleanupStreaming();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.typingIndex >= this.streamBuffer.length) return;
|
|
||||||
|
|
||||||
const bubble = this.shadowRoot.getElementById('streaming-bubble');
|
|
||||||
if (!bubble) return;
|
|
||||||
|
|
||||||
const shouldScroll = this.shouldAutoScroll();
|
|
||||||
|
|
||||||
if (this.streamBuffer[this.typingIndex] === '<') {
|
|
||||||
const tagEnd = this.streamBuffer.indexOf('>', this.typingIndex);
|
|
||||||
if (tagEnd !== -1) {
|
|
||||||
const tag = this.streamBuffer.substring(this.typingIndex, tagEnd + 1);
|
|
||||||
this.currentStreamingMessage.html += tag;
|
|
||||||
this.currentStreamingMessage.text += tag;
|
|
||||||
this.typingIndex = tagEnd + 1;
|
|
||||||
bubble.innerHTML = this.currentStreamingMessage.html + '<span class="text-cursor"></span>';
|
|
||||||
if (shouldScroll) this.scrollToBottom();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const char = this.streamBuffer[this.typingIndex];
|
|
||||||
this.currentStreamingMessage.text += char;
|
|
||||||
this.currentStreamingMessage.html += char;
|
|
||||||
|
|
||||||
bubble.innerHTML = this.currentStreamingMessage.html + '<span class="text-cursor"></span>';
|
|
||||||
|
|
||||||
if (char !== ' ' && char !== '<') {
|
|
||||||
this.playTextBeep();
|
|
||||||
if ('vibrate' in navigator) navigator.vibrate(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.typingIndex++;
|
|
||||||
if (shouldScroll) this.scrollToBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
skipToEnd() {
|
|
||||||
clearInterval(this.typingInterval);
|
|
||||||
|
|
||||||
const bubble = this.shadowRoot.getElementById('streaming-bubble');
|
|
||||||
|
|
||||||
this.currentStreamingMessage.text = this.streamBuffer;
|
|
||||||
this.currentStreamingMessage.html = this.streamBuffer;
|
|
||||||
this.typingIndex = this.streamBuffer.length;
|
|
||||||
|
|
||||||
if (bubble) bubble.innerHTML = this.currentStreamingMessage.html;
|
|
||||||
|
|
||||||
this.scrollToBottom();
|
|
||||||
this.cleanupStreaming();
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupStreaming() {
|
|
||||||
clearInterval(this.typingInterval);
|
|
||||||
this.isReceiving = false;
|
|
||||||
this.streamComplete = false;
|
|
||||||
|
|
||||||
const dialogueInput = this.shadowRoot.getElementById('dialogue-input');
|
|
||||||
const dialogueSend = this.shadowRoot.getElementById('dialogue-send');
|
|
||||||
const attachBtn = this.shadowRoot.getElementById('attach-btn');
|
|
||||||
const clearBtn = this.shadowRoot.getElementById('clear-btn');
|
|
||||||
const bubble = this.shadowRoot.getElementById('streaming-bubble');
|
|
||||||
|
|
||||||
dialogueInput.disabled = false;
|
|
||||||
attachBtn.disabled = false;
|
|
||||||
clearBtn.disabled = false;
|
|
||||||
dialogueSend.textContent = 'SEND';
|
|
||||||
dialogueSend.classList.remove('stop', 'skip');
|
|
||||||
|
|
||||||
if (bubble) {
|
|
||||||
bubble.id = '';
|
|
||||||
bubble.innerHTML = this.currentStreamingMessage.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.streamBuffer = '';
|
|
||||||
this.typingIndex = 0;
|
|
||||||
this.currentRequest = null;
|
|
||||||
this.currentStreamingMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
abortStream() {
|
|
||||||
if (this.currentRequest?.abort) {
|
|
||||||
this.currentRequest.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearInterval(this.typingInterval);
|
|
||||||
|
|
||||||
if (this.currentStreamingMessage) {
|
|
||||||
this.streamBuffer = this.currentStreamingMessage.text || '';
|
|
||||||
this.typingIndex = this.streamBuffer.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cleanupStreaming();
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendMessage() {
|
|
||||||
const dialogueInput = this.shadowRoot.getElementById('dialogue-input');
|
|
||||||
let text = dialogueInput.value.trim();
|
|
||||||
if ((!text && this.attachedFiles.length === 0) || this.isReceiving) return;
|
|
||||||
|
|
||||||
if (this.attachedFiles.length > 0) {
|
|
||||||
const fileBlocks = await Promise.all(
|
|
||||||
this.attachedFiles.map(async (file) => {
|
|
||||||
const content = await this.fileToString(file);
|
|
||||||
return `<file name="${file.name}">${content}</file>`;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
text = text + '\n\n' + fileBlocks.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
dialogueInput.value = '';
|
|
||||||
dialogueInput.style.height = 'auto';
|
|
||||||
|
|
||||||
this.addMessage(text, true);
|
|
||||||
|
|
||||||
this.attachedFiles = [];
|
|
||||||
this.renderAttachedFiles();
|
|
||||||
|
|
||||||
// Send via API with streaming callback 💬
|
|
||||||
this.currentRequest = this.api.sendPetMessage(text, (chunk) => {
|
|
||||||
this.handleStreamChunk(chunk);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle completion/errors with promise
|
|
||||||
try {
|
|
||||||
const response = await this.currentRequest;
|
|
||||||
this.handleStreamComplete(response);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.message !== 'Aborted by user') {
|
|
||||||
console.error('❌ LLM Error:', error);
|
|
||||||
this.addMessage(`Error: ${error.message || 'Something went wrong'}`, false);
|
|
||||||
}
|
|
||||||
this.cleanupStreaming();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('llm-component', LlmComponent);
|
|
||||||
296
public/navi.mjs
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
class Navi {
|
||||||
|
api;
|
||||||
|
connected = false;
|
||||||
|
avatar;
|
||||||
|
icon;
|
||||||
|
info;
|
||||||
|
theme;
|
||||||
|
world;
|
||||||
|
|
||||||
|
#animations;
|
||||||
|
#init;
|
||||||
|
#listeners = new Map();
|
||||||
|
#socket;
|
||||||
|
#secret;
|
||||||
|
#world;
|
||||||
|
|
||||||
|
constructor(api = window.location.origin, secret = '') {
|
||||||
|
this.api = api.replace(/\/$/, '');
|
||||||
|
this.avatar = `${this.api}/avatar`;
|
||||||
|
this.icon = `${this.api}/favicon`;
|
||||||
|
this.#secret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async animations() {
|
||||||
|
if(this.#animations) return this.#animations;
|
||||||
|
await this.init();
|
||||||
|
this.#animations = await fetch(`${this.api}/api/animations`, {
|
||||||
|
headers: this.#secret ? {'Authorization': `Bearer ${this.#secret}`} : {}
|
||||||
|
}).then(resp => {
|
||||||
|
if(!resp.ok) throw new Error(`Invalid Navi API: ${this.api}`);
|
||||||
|
return resp.json();
|
||||||
|
});
|
||||||
|
this.emit('animations', this.#animations);
|
||||||
|
return this.#animations;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if(this.#init) return this.#init;
|
||||||
|
this.#init = new Promise(async (res, rej) => {
|
||||||
|
try {
|
||||||
|
this.info = await fetch(`${this.api}/api/info`, {
|
||||||
|
headers: this.#secret ? {'Authorization': `Bearer ${this.#secret}`} : {}
|
||||||
|
}).then(resp => {
|
||||||
|
if(!resp.ok) throw new Error(`Invalid Navi API: ${this.api}`);
|
||||||
|
return resp.json();
|
||||||
|
});
|
||||||
|
this.theme = this.info.theme;
|
||||||
|
|
||||||
|
this.#socket = io(this.api, {auth: this.#secret ? {token: this.#secret} : null});
|
||||||
|
|
||||||
|
this.#socket.on('llm-stream', (chunk) => {
|
||||||
|
if(this.llmCallback) this.llmCallback(chunk);
|
||||||
|
this.emit('llm:stream', chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#socket.on('llm-response', (data) => {
|
||||||
|
if(this.llmResolve) this.llmResolve(data);
|
||||||
|
this.emit('llm:response', data);
|
||||||
|
this.llmCallback = null;
|
||||||
|
this.llmResolve = null;
|
||||||
|
this.llmReject = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#socket.on('llm-error', (error) => {
|
||||||
|
if(this.llmReject) this.llmReject(error);
|
||||||
|
this.emit('llm:error', error);
|
||||||
|
this.llmCallback = null;
|
||||||
|
this.llmResolve = null;
|
||||||
|
this.llmReject = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.connected = true;
|
||||||
|
this.emit('init');
|
||||||
|
res(this);
|
||||||
|
} catch(err) {
|
||||||
|
rej(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this.#init;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// EVENT SYSTEM
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
on(event, callback) {
|
||||||
|
if(!this.#listeners.has(event)) this.#listeners.set(event, []);
|
||||||
|
this.#listeners.get(event).push(callback);
|
||||||
|
return () => this.off(event, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event, callback) {
|
||||||
|
const callbacks = this.#listeners.get(event);
|
||||||
|
if(callbacks) {
|
||||||
|
const index = callbacks.indexOf(callback);
|
||||||
|
if(index > -1) callbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event, data) {
|
||||||
|
const callbacks = this.#listeners.get(event) || [];
|
||||||
|
callbacks.forEach(cb => cb(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// REST API
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
async sendUserMessage(userId, message) {
|
||||||
|
const response = await fetch(`${this.api}/api/message`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({message})
|
||||||
|
});
|
||||||
|
if(!response.ok) throw new Error('Message failed to send bestie');
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
this.emit('message:sent', {
|
||||||
|
userId,
|
||||||
|
message,
|
||||||
|
result
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async linkPet(petId, targetApiUrl) {
|
||||||
|
const response = await fetch(`${this.api}/api/link`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({targetApiUrl})
|
||||||
|
});
|
||||||
|
if(!response.ok) throw new Error('Link connection is bussin (negatively)');
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
this.emit('pet:linked', {
|
||||||
|
petId,
|
||||||
|
targetApiUrl,
|
||||||
|
result
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// WORLD SOCKET
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
connect(apiOrWorld, world) {
|
||||||
|
let api;
|
||||||
|
if(world) {
|
||||||
|
api = apiOrWorld;
|
||||||
|
} else {
|
||||||
|
api = this.api;
|
||||||
|
world = apiOrWorld;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.world) this.world = {};
|
||||||
|
if(this.#world && this.world.api !== api) {
|
||||||
|
this.#world.disconnect();
|
||||||
|
this.#world = null;
|
||||||
|
}
|
||||||
|
if(!this.#world) this.#world = io(`${api}/world`, {auth: this.#secret ? {token: this.#secret} : null});
|
||||||
|
this.world.api = api;
|
||||||
|
this.world.data = null;
|
||||||
|
this.world.name = world;
|
||||||
|
this.world.players = new Map();
|
||||||
|
|
||||||
|
const callbacks = {
|
||||||
|
move: (x, y) => {
|
||||||
|
this.#world.emit('move', {
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
});
|
||||||
|
},
|
||||||
|
leave: () => {
|
||||||
|
this.#world.disconnect();
|
||||||
|
this.world = null;
|
||||||
|
},
|
||||||
|
onData: (data) => {
|
||||||
|
},
|
||||||
|
onPlayers: (players) => {
|
||||||
|
},
|
||||||
|
onJoined: (player) => {
|
||||||
|
},
|
||||||
|
onMoved: (player) => {
|
||||||
|
},
|
||||||
|
onLeft: (player) => {
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#world.on('data', (data) => {
|
||||||
|
this.world.data = data;
|
||||||
|
callbacks.onData(data);
|
||||||
|
this.emit('world:data', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#world.on('players', (players) => {
|
||||||
|
this.world.players.clear();
|
||||||
|
players.forEach(p => this.world.players.set(p.socketId, p));
|
||||||
|
callbacks.onPlayers(players);
|
||||||
|
this.emit('world:players', players);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#world.on('joined', (player) => {
|
||||||
|
this.world.players.set(player.socketId, player);
|
||||||
|
callbacks.onJoined(player);
|
||||||
|
this.emit('world:joined', player);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#world.on('moved', (data) => {
|
||||||
|
const player = this.world.players.get(data.socketId);
|
||||||
|
if(player) {
|
||||||
|
player.x = data.x;
|
||||||
|
player.y = data.y;
|
||||||
|
}
|
||||||
|
callbacks.onMoved(player);
|
||||||
|
this.emit('world:moved', player);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#world.on('left', (data) => {
|
||||||
|
const player = this.world.players.get(data.socketId);
|
||||||
|
this.world.players.delete(data.socketId);
|
||||||
|
callbacks.onLeft(player);
|
||||||
|
this.emit('world:left', player);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#world.on('error', (error) => {
|
||||||
|
console.error('World error:', error);
|
||||||
|
callbacks.onError(error);
|
||||||
|
this.emit('world:error', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#world.emit('join', {
|
||||||
|
world,
|
||||||
|
api
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit('world:join', world);
|
||||||
|
return callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// LLM
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
ask(message, stream) {
|
||||||
|
this.llmCallback = (chunk) => {
|
||||||
|
if(chunk['emote']) this.emit('emote', chunk['emote']);
|
||||||
|
stream(chunk);
|
||||||
|
};
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
this.llmResolve = resolve;
|
||||||
|
this.llmReject = reject;
|
||||||
|
this.#socket.emit('llm-ask', {message});
|
||||||
|
this.emit('llm:ask');
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.abort = () => {
|
||||||
|
this.#socket.emit('llm-abort');
|
||||||
|
if(this.llmReject) this.llmReject(new Error('Aborted by user'));
|
||||||
|
this.llmCallback = null;
|
||||||
|
this.llmResolve = null;
|
||||||
|
this.llmReject = null;
|
||||||
|
this.emit('llm:abort');
|
||||||
|
};
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearChat() {
|
||||||
|
if(this.#socket) this.#socket.emit('llm-clear');
|
||||||
|
this.emit('llm:clear');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// UTILITY
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.connected = false;
|
||||||
|
this.avatar = this.icon = this.info = this.theme = this.world = this.#animations = this.#init = this.#secret = null;
|
||||||
|
if(this.#world) {
|
||||||
|
this.#world.disconnect();
|
||||||
|
this.#world = null;
|
||||||
|
}
|
||||||
|
if(this.#socket) {
|
||||||
|
this.#socket.disconnect();
|
||||||
|
this.#socket = null;
|
||||||
|
}
|
||||||
|
this.emit('disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Navi;
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
class NetNaviAPI {
|
|
||||||
constructor(baseUrl = window.location.origin) {
|
|
||||||
if(window.netNaviAPI) return window.netNaviAPI;
|
|
||||||
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
||||||
|
|
||||||
// State properties
|
|
||||||
this.petInfo = null;
|
|
||||||
this.spriteSheet = null;
|
|
||||||
this.isConnected = false;
|
|
||||||
this.lastSync = null;
|
|
||||||
this.currentWorld = null;
|
|
||||||
this.currentWorldHost = null;
|
|
||||||
this.currentPlayers = new Map();
|
|
||||||
|
|
||||||
// Socket
|
|
||||||
this.worldSocket = null;
|
|
||||||
this.llmSocket = io(`${this.baseUrl}/llm`);
|
|
||||||
this._setupLLMListeners();
|
|
||||||
this.listeners = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupLLMListeners() {
|
|
||||||
this.llmSocket.on('stream', (chunk) => {
|
|
||||||
this.emit('llm:stream', chunk);
|
|
||||||
if (this.currentStreamCallback) this.currentStreamCallback(chunk);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.llmSocket.on('response', (data) => {
|
|
||||||
this.emit('llm:response', data);
|
|
||||||
if (this.currentResolve) this.currentResolve(data);
|
|
||||||
this.currentStreamCallback = null;
|
|
||||||
this.currentResolve = null;
|
|
||||||
this.currentReject = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.llmSocket.on('error', (error) => {
|
|
||||||
console.error('❌ LLM socket error:', error);
|
|
||||||
this.emit('llm:error', error);
|
|
||||||
if (this.currentReject) this.currentReject(error);
|
|
||||||
this.currentStreamCallback = null;
|
|
||||||
this.currentResolve = null;
|
|
||||||
this.currentReject = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// EVENT SYSTEM
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
on(event, callback) {
|
|
||||||
if (!this.listeners.has(event)) {
|
|
||||||
this.listeners.set(event, []);
|
|
||||||
}
|
|
||||||
this.listeners.get(event).push(callback);
|
|
||||||
return () => this.off(event, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
off(event, callback) {
|
|
||||||
const callbacks = this.listeners.get(event);
|
|
||||||
if (callbacks) {
|
|
||||||
const index = callbacks.indexOf(callback);
|
|
||||||
if (index > -1) callbacks.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(event, data) {
|
|
||||||
const callbacks = this.listeners.get(event) || [];
|
|
||||||
callbacks.forEach(cb => cb(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// REST API
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
async getPetInfo(petId, forceRefresh = false) {
|
|
||||||
if (this.petInfo && !forceRefresh) {
|
|
||||||
return this.petInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/api/info`);
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch PET info fr fr');
|
|
||||||
|
|
||||||
this.petInfo = await response.json();
|
|
||||||
this.lastSync = Date.now();
|
|
||||||
this.emit('petInfo:updated', this.petInfo);
|
|
||||||
|
|
||||||
return this.petInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSpriteSheet(petId, forceRefresh = false) {
|
|
||||||
if (this.spriteSheet && !forceRefresh) {
|
|
||||||
return this.spriteSheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/api/sprite`);
|
|
||||||
if (!response.ok) throw new Error('Sprite sheet is cooked 💀');
|
|
||||||
|
|
||||||
this.spriteSheet = await response.json();
|
|
||||||
this.emit('sprite:updated', this.spriteSheet);
|
|
||||||
|
|
||||||
return this.spriteSheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendUserMessage(userId, message) {
|
|
||||||
const response = await fetch(`${this.baseUrl}/api/message`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ message })
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Message failed to send bestie');
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
this.emit('message:sent', { userId, message, result });
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async linkPet(petId, targetApiUrl) {
|
|
||||||
const response = await fetch(`${this.baseUrl}/api/link`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ targetApiUrl })
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Link connection is bussin (negatively)');
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
this.emit('pet:linked', { petId, targetApiUrl, result });
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// WORLD SOCKET
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
joinWorld(worldId, callbacks = {}) {
|
|
||||||
// Parse the worldId to check if it includes a host
|
|
||||||
let targetHost = this.baseUrl;
|
|
||||||
let actualWorldId = worldId;
|
|
||||||
|
|
||||||
// Check if worldId is a URL (like "http://other-server.com/worldName")
|
|
||||||
if(worldId?.startsWith('http://') || worldId?.startsWith('https://')) {
|
|
||||||
const url = new URL(worldId);
|
|
||||||
targetHost = url.origin;
|
|
||||||
actualWorldId = url.pathname.replace(/^\//, '') || 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leave current world first if we're switching 🚪
|
|
||||||
if (this.worldSocket && this.currentWorld) {
|
|
||||||
this.worldSocket.emit('player-leave'); // Let server know we're bouncing
|
|
||||||
if (this.currentWorldHost !== targetHost) {
|
|
||||||
this.worldSocket.disconnect();
|
|
||||||
this.worldSocket = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize world socket for the target host 🔌
|
|
||||||
if (!this.worldSocket) {
|
|
||||||
this.worldSocket = io(targetHost);
|
|
||||||
this.currentWorldHost = targetHost;
|
|
||||||
this.isConnected = true;
|
|
||||||
this.emit('connection:changed', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentWorld = actualWorldId;
|
|
||||||
|
|
||||||
// Auto-build playerInfo from state 💪
|
|
||||||
const playerInfo = {
|
|
||||||
name: this.petInfo?.name || 'Guest',
|
|
||||||
apiUrl: this.baseUrl
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup socket listeners with provided callbacks 📡
|
|
||||||
this.worldSocket.on('world-data', (data) => {
|
|
||||||
this.emit('world:loaded', data);
|
|
||||||
if (callbacks.onWorldLoaded) callbacks.onWorldLoaded(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.worldSocket.on('current-players', (players) => {
|
|
||||||
this.currentPlayers.clear();
|
|
||||||
players.forEach(p => this.currentPlayers.set(p.socketId, p));
|
|
||||||
this.emit('world:currentPlayers', players);
|
|
||||||
if (callbacks.onCurrentPlayers) callbacks.onCurrentPlayers(players);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.worldSocket.on('player-joined', (player) => {
|
|
||||||
this.currentPlayers.set(player.socketId, player);
|
|
||||||
this.emit('world:playerJoined', player);
|
|
||||||
if (callbacks.onPlayerJoined) callbacks.onPlayerJoined(player);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.worldSocket.on('player-moved', (data) => {
|
|
||||||
const player = this.currentPlayers.get(data.socketId);
|
|
||||||
if (player) {
|
|
||||||
player.x = data.x;
|
|
||||||
player.y = data.y;
|
|
||||||
}
|
|
||||||
this.emit('world:playerMoved', data);
|
|
||||||
if (callbacks.onPlayerMoved) callbacks.onPlayerMoved(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.worldSocket.on('player-left', (data) => {
|
|
||||||
this.currentPlayers.delete(data.socketId);
|
|
||||||
this.emit('world:playerLeft', data);
|
|
||||||
if (callbacks.onPlayerLeft) callbacks.onPlayerLeft(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.worldSocket.on('error', (error) => {
|
|
||||||
console.error('❌ World socket error:', error);
|
|
||||||
this.emit('world:error', error);
|
|
||||||
if (callbacks.onError) callbacks.onError(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Join the world 🌍
|
|
||||||
this.worldSocket.emit('join-world', { worldId: actualWorldId, playerInfo });
|
|
||||||
|
|
||||||
// Return actions dictionary for sending updates 📤
|
|
||||||
return {
|
|
||||||
move: (x, y) => {
|
|
||||||
this.worldSocket.emit('player-move', { x, y });
|
|
||||||
},
|
|
||||||
leave: () => {
|
|
||||||
this.worldSocket.disconnect();
|
|
||||||
this.worldSocket = null;
|
|
||||||
this.currentWorld = null;
|
|
||||||
this.currentWorldHost = null;
|
|
||||||
this.currentPlayers.clear();
|
|
||||||
this.isConnected = false;
|
|
||||||
this.emit('connection:changed', false);
|
|
||||||
},
|
|
||||||
reconnect: () => {
|
|
||||||
if (!this.worldSocket || !this.worldSocket.connected) {
|
|
||||||
this.worldSocket = io(this.currentWorldHost || targetHost);
|
|
||||||
this.worldSocket.emit('join-world', { worldId: actualWorldId, playerInfo });
|
|
||||||
this.isConnected = true;
|
|
||||||
this.emit('connection:changed', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// LLM SOCKET
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
sendPetMessage(message, onStreamCallback) {
|
|
||||||
this.currentStreamCallback = onStreamCallback;
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
|
||||||
this.currentResolve = resolve;
|
|
||||||
this.currentReject = reject;
|
|
||||||
this.llmSocket.emit('message', {message, apiUrl: this.baseUrl});
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.abort = () => {
|
|
||||||
this.llmSocket.emit('abort');
|
|
||||||
if(this.currentReject) this.currentReject(new Error('Aborted by user'));
|
|
||||||
this.currentStreamCallback = null;
|
|
||||||
this.currentResolve = null;
|
|
||||||
this.currentReject = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearLLMHistory() {
|
|
||||||
if(this.llmSocket) {
|
|
||||||
this.llmSocket.emit('clear');
|
|
||||||
this.emit('llm:cleared');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectLLM() {
|
|
||||||
if(this.llmSocket) {
|
|
||||||
this.llmSocket.disconnect();
|
|
||||||
this.llmSocket = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// UTILITY
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
getState() {
|
|
||||||
return {
|
|
||||||
petInfo: this.petInfo,
|
|
||||||
spriteSheet: this.spriteSheet,
|
|
||||||
isConnected: this.isConnected,
|
|
||||||
lastSync: this.lastSync,
|
|
||||||
currentWorld: this.currentWorld,
|
|
||||||
currentPlayers: Array.from(this.currentPlayers.values())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
if (this.worldSocket) {
|
|
||||||
this.worldSocket.disconnect();
|
|
||||||
this.worldSocket = null;
|
|
||||||
}
|
|
||||||
if (this.llmSocket) {
|
|
||||||
this.llmSocket.disconnect();
|
|
||||||
this.llmSocket = null;
|
|
||||||
}
|
|
||||||
this.isConnected = false;
|
|
||||||
this.emit('connection:changed', false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
169
public/tts.mjs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// tts.service.mjs
|
||||||
|
export class TTS {
|
||||||
|
static QUALITY_PATTERNS = ['Google', 'Microsoft', 'Samantha', 'Premium', 'Natural', 'Neural'];
|
||||||
|
static _errorHandlerInstalled = false;
|
||||||
|
static _instance = null;
|
||||||
|
|
||||||
|
_currentUtterance = null;
|
||||||
|
_voicesLoaded;
|
||||||
|
_stoppedUtterances = new WeakSet();
|
||||||
|
_rate = 1;
|
||||||
|
_pitch = 1;
|
||||||
|
_volume = 1;
|
||||||
|
_voice;
|
||||||
|
_hooks = {
|
||||||
|
onSentenceStart: [],
|
||||||
|
onSentenceEnd: [],
|
||||||
|
onChunk: [],
|
||||||
|
onComplete: []
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor({ rate, pitch, volume, voice } = {}) {
|
||||||
|
TTS.installErrorHandler();
|
||||||
|
this._voicesLoaded = this.initializeVoices();
|
||||||
|
if (rate !== undefined) this._rate = rate;
|
||||||
|
if (pitch !== undefined) this._pitch = pitch;
|
||||||
|
if (volume !== undefined) this._volume = volume;
|
||||||
|
this._voice = voice === null ? undefined : voice;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(config) {
|
||||||
|
if(!this._instance) this._instance = new TTS(config);
|
||||||
|
return this._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
static installErrorHandler() {
|
||||||
|
if (this._errorHandlerInstalled) return;
|
||||||
|
window.addEventListener('unhandledrejection', e => {
|
||||||
|
if (e.reason?.error === 'interrupted' && e.reason instanceof SpeechSynthesisErrorEvent) e.preventDefault();
|
||||||
|
});
|
||||||
|
this._errorHandlerInstalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeVoices() {
|
||||||
|
return new Promise(res => {
|
||||||
|
const voices = window.speechSynthesis.getVoices();
|
||||||
|
if (voices.length) {
|
||||||
|
if (!this._voice) this._voice = TTS.bestVoice();
|
||||||
|
res();
|
||||||
|
} else {
|
||||||
|
const h = () => {
|
||||||
|
window.speechSynthesis.removeEventListener('voiceschanged', h);
|
||||||
|
if (!this._voice) this._voice = TTS.bestVoice();
|
||||||
|
res();
|
||||||
|
};
|
||||||
|
window.speechSynthesis.addEventListener('voiceschanged', h);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static bestVoice(lang = 'en') {
|
||||||
|
const voices = window.speechSynthesis.getVoices();
|
||||||
|
for (const p of this.QUALITY_PATTERNS) {
|
||||||
|
const v = voices.find(v => v.name.includes(p) && v.lang.startsWith(lang));
|
||||||
|
if (v) return v;
|
||||||
|
}
|
||||||
|
return voices.find(v => v.lang.startsWith(lang));
|
||||||
|
}
|
||||||
|
|
||||||
|
static cleanText(t) {
|
||||||
|
return removeEmojis(t).replace(/```[\s\S]*?```/g, ' code block ').replace(/[#*_~`]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, callback) {
|
||||||
|
if(this._hooks[event]) this._hooks[event].push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event, callback) {
|
||||||
|
if(this._hooks[event]) this._hooks[event] = this._hooks[event].filter(cb => cb !== callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
_emit(event, data) {
|
||||||
|
if (this._hooks[event]) {
|
||||||
|
this._hooks[event].forEach(cb => {
|
||||||
|
try {
|
||||||
|
cb(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Error in ${event} hook:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createUtterance(t) {
|
||||||
|
const u = new SpeechSynthesisUtterance(TTS.cleanText(t));
|
||||||
|
const v = this._voice || TTS.bestVoice();
|
||||||
|
if (v) u.voice = v;
|
||||||
|
u.rate = this._rate;
|
||||||
|
u.pitch = this._pitch;
|
||||||
|
u.volume = this._volume;
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
async speak(t) {
|
||||||
|
if(!t.trim()) return;
|
||||||
|
await this._voicesLoaded;
|
||||||
|
if(this._currentUtterance && !this._isStreaming) this.stop();
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
this._currentUtterance = this.createUtterance(t);
|
||||||
|
const u = this._currentUtterance;
|
||||||
|
u.onend = () => {
|
||||||
|
this._currentUtterance = null;
|
||||||
|
res();
|
||||||
|
};
|
||||||
|
u.onerror = e => {
|
||||||
|
console.error('❌ Utterance error:', e);
|
||||||
|
this._currentUtterance = null;
|
||||||
|
if (this._stoppedUtterances.has(u) && e.error === 'interrupted') res();
|
||||||
|
else rej(e);
|
||||||
|
};
|
||||||
|
window.speechSynthesis.speak(u);
|
||||||
|
this._emit('onSentenceStart', {sentence: t});
|
||||||
|
}).finally(() => this._emit('onSentenceEnd', {sentence: t}));
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this._currentUtterance) this._stoppedUtterances.add(this._currentUtterance);
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
this._currentUtterance = null;
|
||||||
|
this._isStreaming = false;
|
||||||
|
this._emit('onComplete', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
speakStream() {
|
||||||
|
this._isStreaming = true;
|
||||||
|
let buf = '';
|
||||||
|
let sentenceQueue = Promise.resolve();
|
||||||
|
const rx = /[^.!?\n]+[.!?\n]+/g;
|
||||||
|
|
||||||
|
return {
|
||||||
|
next: t => {
|
||||||
|
buf += t;
|
||||||
|
this._emit('onChunk', { text: t });
|
||||||
|
const ss = buf.match(rx);
|
||||||
|
if(ss) {
|
||||||
|
ss.forEach(s => {
|
||||||
|
const sentence = s.trim();
|
||||||
|
sentenceQueue = sentenceQueue.then(async () => this.speak(sentence));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
buf = buf.replace(rx, '');
|
||||||
|
},
|
||||||
|
done: async () => {
|
||||||
|
if (buf.trim()) {
|
||||||
|
const sentence = buf.trim();
|
||||||
|
sentenceQueue = sentenceQueue.then(async () => this.speak(sentence));
|
||||||
|
buf = '';
|
||||||
|
}
|
||||||
|
await sentenceQueue;
|
||||||
|
this._isStreaming = false;
|
||||||
|
this._emit('onComplete', {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeEmojis(str) {
|
||||||
|
const emojiRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud83c[\udde6-\uddff]|[\ud83d[\ude00-\ude4f]|[\ud83d[\ude80-\udeff]|[\ud83c[\udd00-\uddff]|[\ud83d[\ude50-\ude7f]|[\u2600-\u26ff]|[\u2700-\u27bf]|[\ud83e[\udd00-\uddff]|[\ud83c[\udf00-\uffff]|[\ud83d[\ude00-\udeff]|[\ud83c[\udde6-\uddff])/g;
|
||||||
|
return str.replace(emojiRegex, '');
|
||||||
|
}
|
||||||
60
public/world.html
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>NetNavi v1.0.0</title>
|
||||||
|
<link rel="icon" href="/favicon.png"/>
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
|
||||||
|
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/7.3.2/pixi.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, button, input {
|
||||||
|
cursor: url('/assets/cursor.png'), auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game"></div>
|
||||||
|
<jukebox-component id="jukebox"></jukebox-component>
|
||||||
|
<llm-component id="llm"></llm-component>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import Navi from './navi.mjs';
|
||||||
|
|
||||||
|
window.navi = new Navi();
|
||||||
|
</script>
|
||||||
|
<script type="module" src="/components/jukebox.mjs"></script>
|
||||||
|
<script type="module" src="/components/llm.mjs"></script>
|
||||||
|
<script type="module" src="/components/world.mjs"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
397
public/world.js
@@ -1,397 +0,0 @@
|
|||||||
// ============================================
|
|
||||||
// CONSTANTS
|
|
||||||
// ============================================
|
|
||||||
const TILE_WIDTH = 64;
|
|
||||||
const TILE_HEIGHT = 32;
|
|
||||||
const TILE_DEPTH = 16;
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// HAPTIC FEEDBACK
|
|
||||||
// ============================================
|
|
||||||
function triggerHaptic() {
|
|
||||||
if ('vibrate' in navigator) {
|
|
||||||
navigator.vibrate(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// THEME HANDLER
|
|
||||||
// ============================================
|
|
||||||
function applyTheme(theme) {
|
|
||||||
const body = document.body;
|
|
||||||
|
|
||||||
if (theme.background.image) {
|
|
||||||
body.style.backgroundImage = `url(${theme.background.image})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.style.backgroundSize = theme.background.style || 'cover';
|
|
||||||
body.style.backgroundPosition = 'center';
|
|
||||||
body.style.backgroundRepeat = 'no-repeat';
|
|
||||||
body.style.backgroundAttachment = 'fixed';
|
|
||||||
|
|
||||||
const root = document.documentElement;
|
|
||||||
Object.entries(theme.colors).forEach(([key, value]) => {
|
|
||||||
const cssVar = `--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
|
||||||
root.style.setProperty(cssVar, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('🎨 Theme applied:', theme.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// TILE RENDERER
|
|
||||||
// ============================================
|
|
||||||
function isoToScreen(gridX, gridY) {
|
|
||||||
return {
|
|
||||||
x: (gridX - gridY) * (TILE_WIDTH / 2) + window.innerWidth / 2,
|
|
||||||
y: (gridX + gridY) * (TILE_HEIGHT / 2) + 100
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTile(tileData, theme) {
|
|
||||||
const graphics = new PIXI.Graphics();
|
|
||||||
const pos = isoToScreen(tileData.x, tileData.y);
|
|
||||||
|
|
||||||
const colors = {
|
|
||||||
top: parseInt(theme.colors.tileTop.replace('#', '0x')),
|
|
||||||
side: parseInt(theme.colors.tileSide.replace('#', '0x')),
|
|
||||||
grid: parseInt(theme.colors.gridColor.replace('#', '0x')),
|
|
||||||
highlight: parseInt(theme.colors.tileHighlight.replace('#', '0x')),
|
|
||||||
gridHighlight: parseInt(theme.colors.gridHighlight.replace('#', '0x'))
|
|
||||||
};
|
|
||||||
|
|
||||||
function drawNormalTile() {
|
|
||||||
graphics.clear();
|
|
||||||
graphics.beginFill(colors.top);
|
|
||||||
graphics.lineStyle(1, colors.grid);
|
|
||||||
graphics.moveTo(pos.x, pos.y);
|
|
||||||
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
|
||||||
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.lineTo(pos.x, pos.y);
|
|
||||||
graphics.endFill();
|
|
||||||
|
|
||||||
graphics.beginFill(colors.side);
|
|
||||||
graphics.lineStyle(1, colors.grid);
|
|
||||||
graphics.moveTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
|
||||||
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH);
|
|
||||||
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.endFill();
|
|
||||||
|
|
||||||
graphics.beginFill(colors.side);
|
|
||||||
graphics.lineStyle(1, colors.grid);
|
|
||||||
graphics.moveTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
|
||||||
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH);
|
|
||||||
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.endFill();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawHighlightTile() {
|
|
||||||
graphics.clear();
|
|
||||||
graphics.beginFill(colors.highlight);
|
|
||||||
graphics.lineStyle(2, colors.gridHighlight);
|
|
||||||
graphics.moveTo(pos.x, pos.y);
|
|
||||||
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
|
||||||
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.lineTo(pos.x, pos.y);
|
|
||||||
graphics.endFill();
|
|
||||||
|
|
||||||
graphics.beginFill(colors.side);
|
|
||||||
graphics.lineStyle(1, colors.gridHighlight);
|
|
||||||
graphics.moveTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
|
||||||
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH);
|
|
||||||
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.endFill();
|
|
||||||
|
|
||||||
graphics.beginFill(colors.side);
|
|
||||||
graphics.lineStyle(1, colors.gridHighlight);
|
|
||||||
graphics.moveTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT);
|
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
|
||||||
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH);
|
|
||||||
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
|
||||||
graphics.endFill();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawNormalTile();
|
|
||||||
|
|
||||||
graphics.interactive = true;
|
|
||||||
graphics.buttonMode = true;
|
|
||||||
graphics.gridX = tileData.x;
|
|
||||||
graphics.gridY = tileData.y;
|
|
||||||
|
|
||||||
graphics.on('pointerover', () => {
|
|
||||||
drawHighlightTile();
|
|
||||||
});
|
|
||||||
|
|
||||||
graphics.on('pointerout', () => {
|
|
||||||
drawNormalTile();
|
|
||||||
});
|
|
||||||
|
|
||||||
return graphics;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPet(gridX, gridY, name = 'PET') {
|
|
||||||
const container = new PIXI.Container();
|
|
||||||
const pos = isoToScreen(gridX, gridY);
|
|
||||||
|
|
||||||
const body = new PIXI.Graphics();
|
|
||||||
body.beginFill(0xff6b9d);
|
|
||||||
body.drawCircle(0, -30, 15);
|
|
||||||
body.endFill();
|
|
||||||
|
|
||||||
body.beginFill(0xffffff);
|
|
||||||
body.drawCircle(-5, -32, 4);
|
|
||||||
body.drawCircle(5, -32, 4);
|
|
||||||
body.endFill();
|
|
||||||
|
|
||||||
body.beginFill(0x000000);
|
|
||||||
body.drawCircle(-5, -32, 2);
|
|
||||||
body.drawCircle(5, -32, 2);
|
|
||||||
body.endFill();
|
|
||||||
|
|
||||||
const nameText = new PIXI.Text(name, {
|
|
||||||
fontFamily: 'Courier New',
|
|
||||||
fontSize: 12,
|
|
||||||
fill: '#ffffff',
|
|
||||||
stroke: '#000000',
|
|
||||||
strokeThickness: 2
|
|
||||||
});
|
|
||||||
nameText.anchor.set(0.5);
|
|
||||||
nameText.y = -50;
|
|
||||||
|
|
||||||
container.addChild(body);
|
|
||||||
container.addChild(nameText);
|
|
||||||
container.x = pos.x;
|
|
||||||
container.y = pos.y;
|
|
||||||
container.gridX = gridX;
|
|
||||||
container.gridY = gridY;
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// GAME CLASS
|
|
||||||
// ============================================
|
|
||||||
class Game {
|
|
||||||
constructor() {
|
|
||||||
this.worldId = '';
|
|
||||||
this.theme = null;
|
|
||||||
this.world = null;
|
|
||||||
this.app = null;
|
|
||||||
this.pet = null;
|
|
||||||
this.otherPlayers = new Map();
|
|
||||||
this.isMoving = false;
|
|
||||||
this.dialogue = null;
|
|
||||||
this.keys = {};
|
|
||||||
|
|
||||||
// Use global singleton 🌍
|
|
||||||
this.api = window.netNaviAPI;
|
|
||||||
this.worldActions = null;
|
|
||||||
|
|
||||||
this.playerInfo = {
|
|
||||||
name: 'Guest',
|
|
||||||
apiUrl: this.api.baseUrl
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
try {
|
|
||||||
// Join world with callbacks 🌍
|
|
||||||
this.worldActions = this.api.joinWorld(this.worldId, {
|
|
||||||
onWorldLoaded: (data) => {
|
|
||||||
this.world = data.world;
|
|
||||||
this.theme = data.theme;
|
|
||||||
applyTheme(this.theme);
|
|
||||||
this.initializeRenderer();
|
|
||||||
},
|
|
||||||
|
|
||||||
onCurrentPlayers: (players) => {
|
|
||||||
players.forEach(player => {
|
|
||||||
if (player.socketId !== this.api.worldSocket.id) {
|
|
||||||
this.addOtherPlayer(player);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onPlayerJoined: (player) => {
|
|
||||||
this.addOtherPlayer(player);
|
|
||||||
},
|
|
||||||
|
|
||||||
onPlayerMoved: (data) => {
|
|
||||||
const sprite = this.otherPlayers.get(data.socketId);
|
|
||||||
if (sprite) {
|
|
||||||
this.moveOtherPlayer(sprite, data.x, data.y);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onPlayerLeft: (data) => {
|
|
||||||
const sprite = this.otherPlayers.get(data.socketId);
|
|
||||||
if (sprite) {
|
|
||||||
this.app.stage.removeChild(sprite);
|
|
||||||
this.otherPlayers.delete(data.socketId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('❌ World error:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✨ Game initializing...');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to initialize game:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addOtherPlayer(player) {
|
|
||||||
const sprite = createPet(player.x, player.y, player.name);
|
|
||||||
sprite.alpha = 0.7;
|
|
||||||
this.otherPlayers.set(player.socketId, sprite);
|
|
||||||
this.app.stage.addChild(sprite);
|
|
||||||
}
|
|
||||||
|
|
||||||
moveOtherPlayer(sprite, targetX, targetY) {
|
|
||||||
const targetPos = isoToScreen(targetX, targetY);
|
|
||||||
|
|
||||||
const startX = sprite.x;
|
|
||||||
const startY = sprite.y;
|
|
||||||
let progress = 0;
|
|
||||||
|
|
||||||
const animate = () => {
|
|
||||||
progress += 0.08;
|
|
||||||
if (progress >= 1) {
|
|
||||||
sprite.x = targetPos.x;
|
|
||||||
sprite.y = targetPos.y;
|
|
||||||
sprite.gridX = targetX;
|
|
||||||
sprite.gridY = targetY;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sprite.x = startX + (targetPos.x - startX) * progress;
|
|
||||||
sprite.y = startY + (targetPos.y - startY) * progress;
|
|
||||||
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeRenderer() {
|
|
||||||
this.app = new PIXI.Application({
|
|
||||||
width: window.innerWidth,
|
|
||||||
height: window.innerHeight,
|
|
||||||
backgroundAlpha: 0,
|
|
||||||
antialias: true,
|
|
||||||
resolution: window.devicePixelRatio || 1,
|
|
||||||
autoDensity: true
|
|
||||||
});
|
|
||||||
document.getElementById('game').appendChild(this.app.view);
|
|
||||||
|
|
||||||
const tiles = new PIXI.Container();
|
|
||||||
this.app.stage.addChild(tiles);
|
|
||||||
|
|
||||||
this.world.tiles.forEach(tileData => {
|
|
||||||
const tile = createTile(tileData, this.theme);
|
|
||||||
tile.on('pointerdown', () => this.movePetTo(tile.gridX, tile.gridY));
|
|
||||||
tiles.addChild(tile);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pet = createPet(this.world.pet.startX, this.world.pet.startY, this.playerInfo.name);
|
|
||||||
this.app.stage.addChild(this.pet);
|
|
||||||
|
|
||||||
this.dialogue = document.getElementById('llm');
|
|
||||||
|
|
||||||
this.setupInput();
|
|
||||||
this.app.ticker.add(() => this.gameLoop());
|
|
||||||
}
|
|
||||||
|
|
||||||
movePetTo(targetX, targetY) {
|
|
||||||
if (this.isMoving ||
|
|
||||||
targetX < 0 || targetX >= this.world.gridSize ||
|
|
||||||
targetY < 0 || targetY >= this.world.gridSize) return;
|
|
||||||
|
|
||||||
this.isMoving = true;
|
|
||||||
const targetPos = isoToScreen(targetX, targetY);
|
|
||||||
|
|
||||||
const startX = this.pet.x;
|
|
||||||
const startY = this.pet.y;
|
|
||||||
let progress = 0;
|
|
||||||
|
|
||||||
const animate = () => {
|
|
||||||
progress += 0.08;
|
|
||||||
if (progress >= 1) {
|
|
||||||
this.pet.x = targetPos.x;
|
|
||||||
this.pet.y = targetPos.y;
|
|
||||||
this.pet.gridX = targetX;
|
|
||||||
this.pet.gridY = targetY;
|
|
||||||
this.isMoving = false;
|
|
||||||
|
|
||||||
// Use API action to send move 📤
|
|
||||||
if (this.worldActions) {
|
|
||||||
this.worldActions.move(targetX, targetY);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pet.x = startX + (targetPos.x - startX) * progress;
|
|
||||||
this.pet.y = startY + (targetPos.y - startY) * progress;
|
|
||||||
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupInput() {
|
|
||||||
window.addEventListener('keydown', (e) => {
|
|
||||||
if (this.dialogue.isOpen) return;
|
|
||||||
|
|
||||||
this.keys[e.key.toLowerCase()] = true;
|
|
||||||
|
|
||||||
if (!this.isMoving) {
|
|
||||||
let newX = this.pet.gridX;
|
|
||||||
let newY = this.pet.gridY;
|
|
||||||
|
|
||||||
if (this.keys['w'] || this.keys['arrowup']) {
|
|
||||||
newY--;
|
|
||||||
} else if (this.keys['s'] || this.keys['arrowdown']) {
|
|
||||||
newY++;
|
|
||||||
} else if (this.keys['a'] || this.keys['arrowleft']) {
|
|
||||||
newX--;
|
|
||||||
} else if (this.keys['d'] || this.keys['arrowright']) {
|
|
||||||
newX++;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.movePetTo(newX, newY);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('keyup', (e) => {
|
|
||||||
this.keys[e.key.toLowerCase()] = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
gameLoop() {
|
|
||||||
if (!this.isMoving && this.pet) {
|
|
||||||
this.pet.children[0].y = -30 + Math.sin(Date.now() / 300) * 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// START GAME
|
|
||||||
// ============================================
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const worldId = urlParams.get('world');
|
|
||||||
|
|
||||||
const game = new Game();
|
|
||||||
game.worldId = worldId;
|
|
||||||
game.init();
|
|
||||||
27
src/environment.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import {dirname, join} from 'path';
|
||||||
|
import {fileURLToPath} from 'url';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config({path: ['.env','.env.local'], debug: false, quiet: true});
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = join(dirname(__filename), '..');
|
||||||
|
const storage = join(__dirname, 'storage');
|
||||||
|
|
||||||
|
export const environment = {
|
||||||
|
port: process.env.PORT || 3000,
|
||||||
|
publicUrl: process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 3000}`,
|
||||||
|
llm: {
|
||||||
|
host: process.env.LLM_HOST || '',
|
||||||
|
model: process.env.LLM_MODEL || 'default',
|
||||||
|
token: process.env.LLM_TOKEN || 'ignore',
|
||||||
|
context: process.env.LLM_CONTEXT ? +process.env.LLM_CONTEXT : 60_000.
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
public: join(__dirname, 'public'),
|
||||||
|
storage,
|
||||||
|
navi: join(storage, 'navi'),
|
||||||
|
protocols: join(storage, 'protocols'),
|
||||||
|
worlds: join(storage, 'worlds')
|
||||||
|
}
|
||||||
|
}
|
||||||
451
src/server.js
@@ -4,107 +4,258 @@ import { Server } from 'socket.io';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import {join, dirname} from 'path';
|
import {join, dirname} from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import {Ai, DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool} from '@ztimson/ai-utils';
|
import {Ai, DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool} from '@ztimson/ai-utils';
|
||||||
|
import {contrast, deepCopy, deepMerge, isEqual, shadeColor} from '@ztimson/utils';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import {environment} from './environment.js';
|
||||||
|
import {resize} from './utils.js';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
// ============================================
|
||||||
const __dirname = dirname(__filename);
|
// Settings
|
||||||
|
// ============================================
|
||||||
|
|
||||||
const app = express();
|
const logo = join(environment.paths.navi, 'logo.png');
|
||||||
const httpServer = createServer(app);
|
const avatar = join(environment.paths.navi, 'avatar.png');
|
||||||
|
const sprite = join(environment.paths.navi, 'sprite.png');
|
||||||
|
const settingsFile = join(environment.paths.navi, 'settings.json');
|
||||||
|
const memoriesFile = join(environment.paths.navi, 'memories.json');
|
||||||
|
|
||||||
let memories = [], settings = {};
|
function calcColors(theme) {
|
||||||
const settingsFile = join(__dirname, '../navi', 'settings.json');
|
return {
|
||||||
const memoriesFile = join(__dirname, '../navi', 'memories.json');
|
...theme,
|
||||||
const logoFile = join(__dirname, '../navi', 'logo.png');
|
backgroundContrast: contrast(theme.background),
|
||||||
|
backgroundDark: shadeColor(theme.background, -.1),
|
||||||
|
backgroundLight: shadeColor(theme.background, .1),
|
||||||
|
primaryContrast: contrast(theme.primary),
|
||||||
|
primaryDark: shadeColor(theme.primary, -.1),
|
||||||
|
primaryLight: shadeColor(theme.primary, .1),
|
||||||
|
accentContrast: contrast(theme.accent),
|
||||||
|
accentDark: shadeColor(theme.accent, -.1),
|
||||||
|
accentLight: shadeColor(theme.accent, .1),
|
||||||
|
mutedContrast: contrast(theme.muted),
|
||||||
|
mutedDark: shadeColor(theme.muted, -.1),
|
||||||
|
mutedLight: shadeColor(theme.muted, .1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let orgSettings, orgMemories, memories = [], settings = {
|
||||||
|
name: 'Navi',
|
||||||
|
personality: '- Keep your responses the same length or shorter than the previous user message',
|
||||||
|
animations: {
|
||||||
|
emote: {
|
||||||
|
"dead": {"x": 50, "y": 17},
|
||||||
|
"grey": {},
|
||||||
|
"realization": {"x": 64, "y": -5},
|
||||||
|
"sigh": {"x": 57, "y": 30},
|
||||||
|
"sweat": {"x": 55, "y": 20},
|
||||||
|
"blush": {"x": 43, "y": 25, "r": 15},
|
||||||
|
"mouth": {"x": 53, "y": 31.75, "r": 20},
|
||||||
|
"question": [{"x": 10, "y": 10, "r": -40}, {"x": 26, "y": 6, "r": -20}, {"x": 50, "y": 5, "r": 10}, {"x": 70, "y": 10, "r": 40}],
|
||||||
|
"cry": [{"x": 25.5, "y": 26.75}, {"x": 37, "y": 27.75}],
|
||||||
|
"drool": {"x": 26, "y": 27.5},
|
||||||
|
"love": [{"x": 49, "y": 24, "r": 15}, {"x": 38.5, "y": 23, "r": 15}],
|
||||||
|
"realize": {"x": 65, "y": 12},
|
||||||
|
"stress": {"x": 55, "y": 19},
|
||||||
|
"tear": {"x": 23, "y": 25.5}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
background: '#fff',
|
||||||
|
border: '#000',
|
||||||
|
text: '#252525',
|
||||||
|
primary: '#9f32ef',
|
||||||
|
accent: '#6f16c3',
|
||||||
|
muted: '#a8a8a8',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Saving
|
||||||
|
// ============================================
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
try {
|
try {
|
||||||
settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
|
orgSettings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
|
||||||
|
settings = deepMerge(settings, deepCopy(orgSettings));
|
||||||
} catch { }
|
} catch { }
|
||||||
try {
|
try {
|
||||||
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
|
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
|
||||||
|
orgMemories = deepCopy(memories.map(m => ({...m, embeddings: undefined})));
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
const dir = dirname(settingsFile);
|
const dir = dirname(settingsFile);
|
||||||
if (!fs.existsSync(dir)) {
|
if(!fs.existsSync(dir)) fs.mkdirSync(dir, {recursive: true});
|
||||||
fs.mkdir(dir, { recursive: true }, (err) => {
|
if(!isEqual(orgSettings, settings)) {
|
||||||
if (err) throw err; // Fail loudly if dirs can’t be made 💀
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
||||||
|
orgSettings = deepCopy(orgSettings);
|
||||||
|
}
|
||||||
|
const m = memories.map(m => ({...m, embeddings: undefined}));
|
||||||
|
if(!isEqual(orgMemories, m)) {
|
||||||
fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2));
|
fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2));
|
||||||
|
orgMemories = m;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shutdown() {
|
// ============================================
|
||||||
save();
|
// AI
|
||||||
process.exit(0);
|
// ============================================
|
||||||
}
|
|
||||||
|
|
||||||
load();
|
load();
|
||||||
const ai = new Ai({
|
const ai = new Ai({
|
||||||
llm: {
|
llm: {
|
||||||
models: {
|
models: {[environment.llm.model]: {proto: 'openai', host: environment.llm.host, token: environment.llm.token},},
|
||||||
'Ministral-3': {proto: 'openai', host: 'http://10.69.0.55:11728', token: 'ignore'},
|
compress: {min: environment.llm.context * 0.5, max: environment.llm.context},
|
||||||
},
|
|
||||||
system: `You are a NetNavi, personal assistant & companion. Keep responses short and unstyled. Use your remember tool liberally to store all facts. Adjust your personality with tools based on your interactions with the user.\n\nPersonality:\n${settings.personality || ''}\n\nUser Requests:\n${settings.instructions || ''}`,
|
|
||||||
tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, {
|
tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, {
|
||||||
name: 'adjust_personality',
|
name: 'emote',
|
||||||
description: 'Replace your current personality instructions',
|
description: 'Make your avatar emote',
|
||||||
args: {instructions: {type: 'string', description: 'Bullet point list of how to behave'}},
|
args: {
|
||||||
|
emote: {type: 'string', description: 'Emote to the user', required: true, enum: ['none', ...Object.keys(settings.animations.emote)]}
|
||||||
|
},
|
||||||
|
fn: (args, stream) => {
|
||||||
|
const exists = ['none', ...Object.keys(settings.animations.emote)].includes(args.emote);
|
||||||
|
if(!exists) stream({emote: 'none'});
|
||||||
|
else stream({emote: args.emote});
|
||||||
|
return exists ? 'done!' : `Invalid emote: ${args.emote}`;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
name: 'personalize',
|
||||||
|
description: 'Replace your current personality',
|
||||||
|
args: {
|
||||||
|
instructions: {type: 'string', description: 'Full bullet point list of how to behave', required: true}
|
||||||
|
},
|
||||||
fn: (args) => {
|
fn: (args) => {
|
||||||
settings.personality = args.instructions;
|
settings.personality = args.instructions;
|
||||||
save();
|
|
||||||
return 'done!';
|
return 'done!';
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const systemPrompt = () => {
|
||||||
|
return `Roleplay a NetNavi & companion named ${settings.name}. and follow these steps:
|
||||||
|
1. Start EVERY reply by calling the [emote] tool with the perfect vibe (or none).
|
||||||
|
2. Next, identify facts in the latest user message and immediately call the [remember] tool on each one. Avoid facts about the conversation or the AI.
|
||||||
|
3. If instructed, or the user seems unsatisfied, rewrite your "Personality Rules" and submit it to the [personalize] tool.
|
||||||
|
4. If asked, you can access your ${os.platform()} workspace using the [exec] tool. Always start from \`${os.tmpdir()}\`.
|
||||||
|
5. Create an unstyled response according to your "Personality Rules"
|
||||||
|
|
||||||
|
Personality Rules:
|
||||||
|
${settings.personality || ''}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Setup
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const httpServer = createServer(app);
|
||||||
const io = new Server(httpServer, {
|
const io = new Server(httpServer, {
|
||||||
cors: {origin: "*", methods: ["GET", "POST"]}
|
cors: {
|
||||||
|
origin: '*',
|
||||||
|
methods: ['GET', 'POST']
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static('public'));
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// WORLD MANAGEMENT
|
// Socket
|
||||||
// ============================================
|
// ============================================
|
||||||
const worldPlayers = new Map();
|
|
||||||
const chatHistory = new Map();
|
const chatHistory = new Map();
|
||||||
|
|
||||||
// Load world data
|
io.on('connection', (socket) => {
|
||||||
function loadWorld(worldId) {
|
console.debug('👤 User connected:', socket.id);
|
||||||
try {
|
|
||||||
const worldPath = join(__dirname, '../worlds', worldId || '', 'world.json');
|
chatHistory.set(socket.id, []);
|
||||||
const world = JSON.parse(fs.readFileSync(worldPath, 'utf-8'));
|
let currentRequest = null;
|
||||||
const themePath = join(__dirname, '../worlds', worldId || '', world.theme);
|
|
||||||
const theme = JSON.parse(fs.readFileSync(themePath, 'utf-8'));
|
socket.on('llm-clear', async () => {
|
||||||
worldPlayers.set(worldId, new Map());
|
chatHistory.set(socket.id, []);
|
||||||
return {world, theme};
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load world ${worldId}:`, error);
|
socket.on('llm-abort', () => {
|
||||||
return null;
|
if(currentRequest?.abort) {
|
||||||
}
|
currentRequest.abort();
|
||||||
|
currentRequest = null;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('llm-ask', async (data) => {
|
||||||
|
const {message} = data;
|
||||||
|
const history = chatHistory.get(socket.id);
|
||||||
|
currentRequest = ai.language.ask(message, {
|
||||||
|
history,
|
||||||
|
memory: memories,
|
||||||
|
system: systemPrompt(),
|
||||||
|
stream: (chunk) => socket.emit('llm-stream', chunk)
|
||||||
|
}).then(resp => {
|
||||||
|
chatHistory.set(socket.id, history);
|
||||||
|
socket.emit('llm-response', {message: resp});
|
||||||
|
}).catch(err => {
|
||||||
|
socket.emit('llm-error', {message: err.message || err.toString()});
|
||||||
|
}).finally(() => {
|
||||||
|
currentRequest = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('👤 User disconnected:', socket.id);
|
||||||
|
chatHistory.delete(socket.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// SOCKET.IO - WORLD CHANNELS
|
// World Connection
|
||||||
// ============================================
|
// ============================================
|
||||||
io.on('connection', (socket) => {
|
const worldPlayers = new Map();
|
||||||
console.debug('🔌 Client connected:', socket.id);
|
|
||||||
|
// Load world data
|
||||||
|
function loadWorld(name) {
|
||||||
|
let w,
|
||||||
|
t;
|
||||||
|
try {
|
||||||
|
w = JSON.parse(fs.readFileSync(join(worlds, (name || 'home') + '.json'), 'utf-8'));
|
||||||
|
} catch(error) {
|
||||||
|
console.error(`Failed to load world ${name}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
t = JSON.parse(fs.readFileSync(join(protocols, w.theme + '.json'), 'utf-8'));
|
||||||
|
t.colors = calcColors(t.colors);
|
||||||
|
} catch(error) {
|
||||||
|
console.error(`Failed to load theme protocol ${w.theme}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
worldPlayers.set(name, new Map());
|
||||||
|
return {
|
||||||
|
...w,
|
||||||
|
theme: t
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
io.of('/world').on('connection', (socket) => {
|
||||||
|
console.debug('🌍 Navi joined world:', socket.id);
|
||||||
|
|
||||||
let currentWorld = null;
|
let currentWorld = null;
|
||||||
let playerData = null;
|
let playerData = null;
|
||||||
|
|
||||||
// Join a world
|
// Join a world
|
||||||
socket.on('join-world', (data) => {
|
socket.on('join', async (data) => {
|
||||||
const { worldId, playerInfo } = data;
|
const {
|
||||||
const worldData = loadWorld(worldId);
|
world,
|
||||||
|
api
|
||||||
|
} = data;
|
||||||
|
const info = await fetch(api.replace(/\/$/, '') + '/api/info').then(resp => {
|
||||||
|
if(resp.ok) return resp.json();
|
||||||
|
socket.emit('error', {message: `Invalid Navi API: ${api}`});
|
||||||
|
return resp.error;
|
||||||
|
});
|
||||||
|
|
||||||
|
const worldData = loadWorld(world);
|
||||||
if(!worldData) return socket.emit('error', {message: 'World not found'});
|
if(!worldData) return socket.emit('error', {message: 'World not found'});
|
||||||
|
|
||||||
// Leave previous world if any
|
// Leave previous world if any
|
||||||
@@ -113,130 +264,114 @@ io.on('connection', (socket) => {
|
|||||||
const players = worldPlayers.get(currentWorld);
|
const players = worldPlayers.get(currentWorld);
|
||||||
if(players) {
|
if(players) {
|
||||||
players.delete(socket.id);
|
players.delete(socket.id);
|
||||||
socket.to(`world:${currentWorld}`).emit('player-left', {socketId: socket.id});
|
socket.to(`world:${currentWorld}`).emit('left', {socketId: socket.id});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join new world
|
// Join new world
|
||||||
currentWorld = worldId;
|
currentWorld = world;
|
||||||
|
const spawn = worldData.tiles.find(t => t.type === 'spawn');
|
||||||
playerData = {
|
playerData = {
|
||||||
|
...info,
|
||||||
socketId: socket.id,
|
socketId: socket.id,
|
||||||
name: playerInfo.name,
|
navi: api,
|
||||||
apiUrl: playerInfo.apiUrl,
|
x: spawn.x,
|
||||||
x: worldData.world.pet.startX,
|
y: spawn.y
|
||||||
y: worldData.world.pet.startY
|
|
||||||
};
|
};
|
||||||
socket.join(`world:${worldId}`);
|
socket.join(`world:${world}`);
|
||||||
const players = worldPlayers.get(worldId);
|
const players = worldPlayers.get(world);
|
||||||
players.set(socket.id, playerData);
|
players.set(socket.id, playerData);
|
||||||
socket.emit('world-data', worldData);
|
socket.emit('data', worldData);
|
||||||
const currentPlayers = Array.from(players.values());
|
const currentPlayers = Array.from(players.values());
|
||||||
socket.emit('current-players', currentPlayers);
|
socket.emit('players', currentPlayers);
|
||||||
socket.to(`world:${worldId}`).emit('player-joined', playerData);
|
socket.to(`world:${world}`).emit('joined', playerData);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Player movement
|
// Player movement
|
||||||
socket.on('player-move', (data) => {
|
socket.on('move', (data) => {
|
||||||
if(!currentWorld || !playerData) return;
|
if(!currentWorld || !playerData) return;
|
||||||
const { x, y } = data;
|
const {
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
} = data;
|
||||||
playerData.x = x;
|
playerData.x = x;
|
||||||
playerData.y = y;
|
playerData.y = y;
|
||||||
socket.to(`world:${currentWorld}`).emit('player-moved', {socketId: socket.id, x, y});
|
socket.to(`world:${currentWorld}`).emit('moved', {
|
||||||
|
socketId: socket.id,
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disconnect
|
// Disconnect
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
console.debug('🔌 Client disconnected:', socket.id);
|
console.debug('🌍 Navi disconnected:', socket.id);
|
||||||
if(currentWorld) {
|
if(currentWorld) {
|
||||||
const players = worldPlayers.get(currentWorld);
|
const players = worldPlayers.get(currentWorld);
|
||||||
if(players) {
|
if(players) {
|
||||||
players.delete(socket.id);
|
players.delete(socket.id);
|
||||||
socket.to(`world:${currentWorld}`).emit('player-left', {socketId: socket.id});
|
socket.to(`world:${currentWorld}`).emit('left', {socketId: socket.id});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// LLM CHANNEL
|
// API ENDPOINTS
|
||||||
// ============================================
|
// ============================================
|
||||||
const petNamespace = io.of('/llm');
|
|
||||||
petNamespace.on('connection', (socket) => {
|
|
||||||
chatHistory.set(socket.id, []);
|
|
||||||
let currentRequest = null;
|
|
||||||
|
|
||||||
socket.on('clear', async () => {
|
app.get('/avatar', (req, res) => {
|
||||||
chatHistory.set(socket.id, []);
|
res.sendFile(avatar);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('abort', () => {
|
app.get('/favicon*', async (req, res) => {
|
||||||
if (currentRequest?.abort) {
|
let w = 256, h = w;
|
||||||
currentRequest.abort();
|
if(req.query && req.query['size']) [w, h] = req.query['size'].split('x');
|
||||||
currentRequest = null;
|
res.contentType('image/png').set('Content-Disposition', 'inline');
|
||||||
|
res.send(await resize(logo, w, h));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
id: environment.publicUrl,
|
||||||
|
short_name: 'NetNavi',
|
||||||
|
name: 'NetNavi',
|
||||||
|
description: 'Network Navigation Program',
|
||||||
|
display: 'standalone',
|
||||||
|
start_url: '/',
|
||||||
|
background_color: settings.theme.accent,
|
||||||
|
theme_color: settings.theme.primary,
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
"src": `${environment.publicUrl}/favicon?size=192x192`,
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": `${environment.publicUrl}/favicon?size=512x512`,
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"purpose": "any"
|
||||||
}
|
}
|
||||||
});
|
]
|
||||||
|
|
||||||
socket.on('message', async (data) => {
|
|
||||||
const { message, apiUrl } = data;
|
|
||||||
const history = chatHistory.get(socket.id);
|
|
||||||
currentRequest = ai.language.ask(message, {
|
|
||||||
history,
|
|
||||||
memory: memories,
|
|
||||||
stream: (chunk) => socket.emit('stream', chunk)
|
|
||||||
}).then(resp => {
|
|
||||||
chatHistory.set(socket.id, history);
|
|
||||||
socket.emit('response', { message: resp });
|
|
||||||
}).catch(err => {
|
|
||||||
socket.emit('error', {message: err.message || err.toString()});
|
|
||||||
}).finally(() => {
|
|
||||||
currentRequest = null;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
app.get('/spritesheet', (req, res) => {
|
||||||
console.log('🔌 LLM Client disconnected:', socket.id);
|
res.sendFile(sprite);
|
||||||
chatHistory.delete(socket.id);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// REST API ENDPOINTS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
app.get('/favicon.*', (req, res) => {
|
|
||||||
res.sendFile(logoFile);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get PET info
|
|
||||||
app.get('/api/info', (req, res) => {
|
app.get('/api/info', (req, res) => {
|
||||||
const { petId } = req.params;
|
|
||||||
// TODO: Fetch from database
|
|
||||||
res.json({
|
res.json({
|
||||||
id: petId,
|
name: settings.name,
|
||||||
name: 'MyCoolPET',
|
avatar: settings.avatar,
|
||||||
owner: 'player1',
|
theme: calcColors(settings.theme),
|
||||||
bandwidth: 75,
|
|
||||||
shards: [],
|
|
||||||
stats: {
|
|
||||||
level: 5,
|
|
||||||
health: 100
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get sprite sheet
|
app.get('/api/animations', (req, res) => {
|
||||||
app.get('/api/sprite', (req, res) => {
|
res.json(settings.animations);
|
||||||
const { petId } = req.params;
|
|
||||||
// TODO: Return actual sprite sheet URL
|
|
||||||
res.json({
|
|
||||||
spriteUrl: '/sprites/default-pet.png',
|
|
||||||
frameWidth: 32,
|
|
||||||
frameHeight: 32,
|
|
||||||
animations: {
|
|
||||||
idle: { frames: [0, 1, 2, 3], speed: 200 },
|
|
||||||
walk: { frames: [4, 5, 6, 7], speed: 100 }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send message to user (push notification / email / etc)
|
// Send message to user (push notification / email / etc)
|
||||||
@@ -244,7 +379,10 @@ app.post('/api/message', (req, res) => {
|
|||||||
const {userId} = req.params;
|
const {userId} = req.params;
|
||||||
const {message} = req.body;
|
const {message} = req.body;
|
||||||
// TODO: Implement notification system
|
// TODO: Implement notification system
|
||||||
res.json({ success: true, message: 'Message sent' });
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Message sent'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send message to PET LLM
|
// Send message to PET LLM
|
||||||
@@ -252,7 +390,10 @@ app.post('/api/message', (req, res) => {
|
|||||||
const {petId} = req.params;
|
const {petId} = req.params;
|
||||||
const {message} = req.body;
|
const {message} = req.body;
|
||||||
// TODO: Queue message for LLM processing
|
// TODO: Queue message for LLM processing
|
||||||
res.json({ success: true, message: 'Message queued' });
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Message queued'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Link another PET
|
// Link another PET
|
||||||
@@ -260,19 +401,51 @@ app.post('/api/link', (req, res) => {
|
|||||||
const {petId} = req.params;
|
const {petId} = req.params;
|
||||||
const {targetApiUrl} = req.body;
|
const {targetApiUrl} = req.body;
|
||||||
// TODO: Store link in database
|
// TODO: Store link in database
|
||||||
res.json({success: true, message: 'PET linked'});
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'PET linked'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
let p = (!req.path || req.path.endsWith('/'))
|
||||||
|
? req.path + (req.path.endsWith('/') ? '' : '/') + 'index.html'
|
||||||
|
: req.path + (req.path.split('/').pop().includes('.') ? '' : '.html');
|
||||||
|
p = path.join(environment.paths.public, p);
|
||||||
|
const data = await fs.readFileSync(p);
|
||||||
|
|
||||||
|
// Only process HTML files
|
||||||
|
if(p.endsWith('.html')) {
|
||||||
|
let body = data.toString('utf-8')
|
||||||
|
.replaceAll('{{PUBLIC_URL}}', environment.publicUrl)
|
||||||
|
.replaceAll('{{NAME}}', settings.name)
|
||||||
|
.replaceAll('{{THEME_BACKGROUND}}', settings.theme.background)
|
||||||
|
.replaceAll('{{THEME_PRIMARY}}', settings.theme.primary)
|
||||||
|
.replaceAll('{{THEME_ACCENT}}', settings.theme.accent);
|
||||||
|
res.set('Content-Type', 'text/html');
|
||||||
|
return res.send(body);
|
||||||
|
}
|
||||||
|
return res.sendFile(p);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'ENOENT') return next();
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// START SERVER
|
// START SERVER
|
||||||
// ============================================
|
// ============================================
|
||||||
const PORT = process.env.PORT || 3000;
|
setInterval(() => save(), 5 * 60_000);
|
||||||
|
httpServer.listen(environment.port, () => {
|
||||||
httpServer.listen(PORT, () => {
|
console.log(`🚀 Server running on: http://localhost:${environment.port}`);
|
||||||
loadWorld();
|
|
||||||
console.log('✅ Home world loaded');
|
|
||||||
console.log(`🚀 Server running on: http://localhost:${PORT}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function shutdown() {
|
||||||
|
save();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
process.on('SIGINT', shutdown);
|
process.on('SIGINT', shutdown);
|
||||||
process.on('SIGTERM', shutdown);
|
process.on('SIGTERM', shutdown);
|
||||||
|
|||||||
9
src/utils.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
export async function resize(image, width, height) {
|
||||||
|
width = Math.round(+width);
|
||||||
|
if(height) height = Math.round(+height);
|
||||||
|
if(width < 1) throw new Error(`Invalid dimensions: ${width}x${height}`);
|
||||||
|
if(width > 1920 || (height && height > 1920)) throw new Error(`Largest dimension supported is 1920: ${width}x${height}`);
|
||||||
|
return await sharp(image).resize({width: width, height: height || width, fit: 'contain'}).toBuffer();
|
||||||
|
}
|
||||||
BIN
storage/navi/avatar.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
26
storage/protocols/theme_default.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "Eva’s Glade",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "theme",
|
||||||
|
"icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><path d=\"M535.3 70.7C541.7 64.6 551 62.4 559.6 65.2C569.4 68.5 576 77.7 576 88L576 274.9C576 406.1 467.9 512 337.2 512C260.2 512 193.8 462.5 169.7 393.3C134.3 424.1 112 469.4 112 520C112 533.3 101.3 544 88 544C74.7 544 64 533.3 64 520C64 445.1 102.2 379.1 160.1 340.3C195.4 316.7 237.5 304 280 304L360 304C373.3 304 384 293.3 384 280C384 266.7 373.3 256 360 256L280 256C240.3 256 202.7 264.8 169 280.5C192.3 210.5 258.2 160 336 160C402.4 160 451.8 137.9 484.7 116C503.9 103.2 520.2 87.9 535.4 70.7z\"/></svg>",
|
||||||
|
|
||||||
|
"background": {
|
||||||
|
"image": "/assets/background.jpg",
|
||||||
|
"style": "cover"
|
||||||
|
},
|
||||||
|
"music": ["/assets/music.mp3"],
|
||||||
|
"colors": {
|
||||||
|
"tileTop": "#7a5a8c",
|
||||||
|
"tileSide": "#4a2d5a",
|
||||||
|
"tileHighlight": "#a17acf",
|
||||||
|
"gridColor": "#5e2f6a",
|
||||||
|
"gridHighlight": "#ff75b5",
|
||||||
|
"background": "#e6e6fa",
|
||||||
|
"border": "#000000",
|
||||||
|
"text": "#2d2524",
|
||||||
|
"dialogueInputBg": "#f0e6ff",
|
||||||
|
"primary": "#aa33ff",
|
||||||
|
"accent": "#8a2be2",
|
||||||
|
"muted": "#ff75b5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "Home World",
|
"name": "Home World",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"theme": "./theme.json",
|
"theme": "theme_default",
|
||||||
|
|
||||||
"gridSize": 8,
|
"gridSize": 8,
|
||||||
"tiles": [
|
"tiles": [
|
||||||
{"x": 0, "y": 0, "type": "floor"},
|
{"x": 0, "y": 0, "type": "floor"},
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
{"x": 1, "y": 4, "type": "floor"},
|
{"x": 1, "y": 4, "type": "floor"},
|
||||||
{"x": 2, "y": 4, "type": "floor"},
|
{"x": 2, "y": 4, "type": "floor"},
|
||||||
{"x": 3, "y": 4, "type": "floor"},
|
{"x": 3, "y": 4, "type": "floor"},
|
||||||
{"x": 4, "y": 4, "type": "floor"},
|
{"x": 4, "y": 4, "type": "spawn"},
|
||||||
{"x": 5, "y": 4, "type": "floor"},
|
{"x": 5, "y": 4, "type": "floor"},
|
||||||
{"x": 6, "y": 4, "type": "floor"},
|
{"x": 6, "y": 4, "type": "floor"},
|
||||||
{"x": 7, "y": 4, "type": "floor"},
|
{"x": 7, "y": 4, "type": "floor"},
|
||||||
@@ -68,9 +69,5 @@
|
|||||||
{"x": 5, "y": 7, "type": "floor"},
|
{"x": 5, "y": 7, "type": "floor"},
|
||||||
{"x": 6, "y": 7, "type": "floor"},
|
{"x": 6, "y": 7, "type": "floor"},
|
||||||
{"x": 7, "y": 7, "type": "floor"}
|
{"x": 7, "y": 7, "type": "floor"}
|
||||||
],
|
]
|
||||||
"pet": {
|
|
||||||
"startX": 4,
|
|
||||||
"startY": 4
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Eva’s Glade 🌿",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"type": "theme",
|
|
||||||
"background": {
|
|
||||||
"image": "/assets/background.jpg",
|
|
||||||
"style": "cover"
|
|
||||||
},
|
|
||||||
"music": "/assets/music.mp3",
|
|
||||||
"colors": {
|
|
||||||
"tileTop": "#7a5a8c",
|
|
||||||
"tileSide": "#4a2d5a",
|
|
||||||
"tileHighlight": "#a17acf",
|
|
||||||
"gridColor": "#5e2f6a",
|
|
||||||
"gridHighlight": "#ff75b5",
|
|
||||||
"dialogueBg": "#e6e6fa",
|
|
||||||
"dialogueBorder": "#000000",
|
|
||||||
"dialogueHeaderBg": "#aa33ff",
|
|
||||||
"dialogueInputBg": "#f0e6ff",
|
|
||||||
"dialogueText": "#2d2524",
|
|
||||||
"buttonBg": "#8a2be2",
|
|
||||||
"buttonText": "#ffffff",
|
|
||||||
"buttonShadow": "#5a3a7d",
|
|
||||||
"muteButtonBg": "#ff75b5",
|
|
||||||
"muteButtonBorder": "#a17acf"
|
|
||||||
}
|
|
||||||
}
|
|
||||||