From b67d3a8454ac6b50ba4ea5a945ea8515ccba997c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81?= Date: Sat, 9 May 2026 00:42:33 +0200 Subject: [PATCH] =?UTF-8?q?Mejorar=20un=20poquito=20el=20dise=C3=B1o=20y?= =?UTF-8?q?=20meterle=20voz=20usando=20whisper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1029 ++++++++++++++++++++++++++++++ package.json | 1 + src/components/Login.js | 18 +- src/components/Sidebar.js | 19 +- src/components/VoiceAssistant.js | 332 ++++++++++ src/main.js | 1 + src/router.js | 44 +- src/services/voiceInput.js | 190 ++++++ src/style.css | 91 +++ src/styles/facturas.css | 28 +- src/styles/login.css | 54 +- src/styles/sidebar.css | 116 +++- src/styles/voice-assistant.css | 302 +++++++++ vite.config.js | 7 + 14 files changed, 2211 insertions(+), 21 deletions(-) create mode 100644 src/components/VoiceAssistant.js create mode 100644 src/services/voiceInput.js create mode 100644 src/styles/voice-assistant.css create mode 100644 vite.config.js diff --git a/package-lock.json b/package-lock.json index 3dbed70..f48554d 100755 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,23 @@ "name": "doli-front", "version": "0.0.0", "dependencies": { + "@huggingface/transformers": "^3.8.1", "chart.js": "^4.5.1" }, "devDependencies": { "vite": "^7.2.4" } }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -456,12 +467,574 @@ "node": ">=18" } }, + "node_modules/@huggingface/jinja": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz", + "integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/transformers": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz", + "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.3", + "onnxruntime-node": "1.21.0", + "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", + "sharp": "^0.34.1" + } + }, + "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/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", @@ -819,6 +1392,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/chart.js": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", @@ -831,6 +1420,88 @@ "pnpm": ">=8" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -873,6 +1544,18 @@ "@esbuild/win32-x64": "0.27.2" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -891,6 +1574,12 @@ } } }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -906,6 +1595,114 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -925,6 +1722,58 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", + "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", + "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "global-agent": "^3.0.0", + "onnxruntime-common": "1.21.0", + "tar": "^7.0.1" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -945,6 +1794,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -974,6 +1829,47 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/protobufjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", + "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.1", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/rollup": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", @@ -1019,6 +1915,83 @@ "fsevents": "~2.3.2" } }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "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/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1029,6 +2002,28 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/tar": { + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1046,6 +2041,31 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "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/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -1120,6 +2140,15 @@ "optional": true } } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } } } } diff --git a/package.json b/package.json index 3f7e936..b3fb271 100755 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "vite": "^7.2.4" }, "dependencies": { + "@huggingface/transformers": "^3.8.1", "chart.js": "^4.5.1" } } diff --git a/src/components/Login.js b/src/components/Login.js index 51222c0..a8b38b2 100755 --- a/src/components/Login.js +++ b/src/components/Login.js @@ -4,15 +4,27 @@ export function renderLogin(onLogin) { loginContainer.innerHTML = `
- + + +
- +
- +
diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js index dd25251..7b87261 100755 --- a/src/components/Sidebar.js +++ b/src/components/Sidebar.js @@ -6,7 +6,17 @@ export function createSidebar(pages, currentPage) { const sidebarHeader = document.createElement('div'); sidebarHeader.className = 'sidebar-header'; sidebarHeader.innerHTML = /*html*/` -

Doli

+ @@ -38,10 +48,13 @@ export function createSidebar(pages, currentPage) { sidebar.appendChild(sidebarHeader); sidebar.appendChild(nav); - // Toggle functionality const toggleBtn = sidebarHeader.querySelector('#sidebar-toggle'); toggleBtn.addEventListener('click', () => { - sidebar.classList.toggle('collapsed'); + if (window.matchMedia('(max-width: 768px)').matches) { + document.dispatchEvent(new CustomEvent('sidebar:close-mobile')); + } else { + sidebar.classList.toggle('collapsed'); + } }); return sidebar; diff --git a/src/components/VoiceAssistant.js b/src/components/VoiceAssistant.js new file mode 100644 index 0000000..4c94581 --- /dev/null +++ b/src/components/VoiceAssistant.js @@ -0,0 +1,332 @@ +import { pipeline } from '@huggingface/transformers'; + +// ── Modelo (singleton entre sesiones) ───────────────────────────────────────── +// Xenova/whisper-tiny + transformers.js v3 = WASM puro, sin WebGPU, todos los navegadores +let transcriberPromise = null; + +function loadModel(onProgress) { + if (!transcriberPromise) { + transcriberPromise = pipeline( + 'automatic-speech-recognition', + 'Xenova/whisper-tiny', + { progress_callback: onProgress } + ).catch(err => { transcriberPromise = null; throw err; }); + } + return transcriberPromise; +} + +// ── Comandos de voz ─────────────────────────────────────────────────────────── +const COMMANDS = [ + { + label: 'Nueva factura', + hint: '"nueva factura" · "crear factura"', + icon: ``, + patterns: ['nueva factura', 'crear factura', 'factura nueva', 'añadir factura', 'nueva facturas'], + action: () => { window.location.hash = '#create-invoice'; }, + }, + { + label: 'Ver facturas', + hint: '"facturas" · "ver facturas"', + icon: ``, + patterns: ['facturas', 'ver facturas', 'ir a facturas', 'lista facturas', 'mis facturas', 'listado facturas'], + action: () => { window.location.hash = '#invoices'; }, + }, + { + label: 'Ver clientes', + hint: '"clientes" · "ver clientes"', + icon: ``, + patterns: ['clientes', 'ver clientes', 'ir a clientes', 'lista clientes', 'mis clientes'], + action: () => { window.location.hash = '#clients'; }, + }, + { + label: 'Dashboard', + hint: '"dashboard" · "inicio" · "resumen"', + icon: ``, + patterns: ['dashboard', 'inicio', 'ir al inicio', 'ir al dashboard', 'resumen', 'home', 'panel', 'principal'], + action: () => { window.location.hash = '#dashboard'; }, + }, + { + label: 'Configuración', + hint: '"configuración" · "ajustes"', + icon: ``, + patterns: ['configuración', 'configuracion', 'ajustes', 'settings', 'preferencias'], + action: () => { window.location.hash = '#settings'; }, + }, +]; + +function matchCommand(text) { + const norm = text.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').trim(); + for (const cmd of COMMANDS) { + for (const pattern of cmd.patterns) { + const normPattern = pattern.normalize('NFD').replace(/[̀-ͯ]/g, ''); + if (norm.includes(normPattern)) return cmd; + } + } + return null; +} + +// ── Grabación ───────────────────────────────────────────────────────────────── +async function startRecording() { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); + const mimeType = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus'] + .find(t => MediaRecorder.isTypeSupported(t)) || ''; + const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : {}); + const chunks = []; + recorder.ondataavailable = e => { if (e.data?.size > 0) chunks.push(e.data); }; + recorder.start(200); + return { + stop: () => new Promise(resolve => { + recorder.onstop = () => { + stream.getTracks().forEach(t => t.stop()); + resolve(new Blob(chunks, { type: mimeType || 'audio/webm' })); + }; + if (recorder.state !== 'inactive') recorder.stop(); + else resolve(new Blob(chunks)); + }), + }; +} + +async function blobToFloat32(blob) { + const arrayBuffer = await blob.arrayBuffer(); + if (arrayBuffer.byteLength === 0) throw new Error('Blob de audio vacío'); + + // 1) Decodificar al sample rate nativo del archivo (p.ej. 44100 Hz) + const decodeCtx = new AudioContext(); + let nativeBuffer; + try { + nativeBuffer = await decodeCtx.decodeAudioData(arrayBuffer); + } finally { + await decodeCtx.close(); + } + + const srcRate = nativeBuffer.sampleRate; + const srcData = nativeBuffer.getChannelData(0); // mono + + // 2) Si ya está a 16 kHz no hace falta resampling + if (srcRate === 16000) return srcData; + + // 3) Resampling a 16 kHz con OfflineAudioContext + const targetLength = Math.ceil(srcData.length * (16000 / srcRate)); + const offCtx = new OfflineAudioContext(1, targetLength, 16000); + + const srcBuf = offCtx.createBuffer(1, srcData.length, srcRate); + srcBuf.getChannelData(0).set(srcData); + + const source = offCtx.createBufferSource(); + source.buffer = srcBuf; + source.connect(offCtx.destination); + source.start(0); + + const resampled = await offCtx.startRendering(); + return resampled.getChannelData(0); +} + +// ── Componente ──────────────────────────────────────────────────────────────── +let assistantEl = null; + +export function mountVoiceAssistant() { + if (assistantEl) { assistantEl.style.display = ''; return; } + if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) return; + + // ── DOM ──────────────────────────────────────────────────────────────────── + assistantEl = document.createElement('div'); + assistantEl.className = 'va-root'; + assistantEl.innerHTML = /*html*/` + + + + `; + + document.body.appendChild(assistantEl); + + // ── Poblar lista de comandos ──────────────────────────────────────────────── + const cmdList = assistantEl.querySelector('#va-commands-list'); + COMMANDS.forEach(cmd => { + const li = document.createElement('li'); + li.className = 'va-cmd-item'; + li.innerHTML = /*html*/` + ${cmd.icon} + + ${cmd.label} + ${cmd.hint} + + `; + li.addEventListener('click', () => { cmd.action(); closePanel(); }); + cmdList.appendChild(li); + }); + + // ── Referencias ──────────────────────────────────────────────────────────── + const fab = assistantEl.querySelector('#va-fab'); + const panel = assistantEl.querySelector('#va-panel'); + const closeBtn = assistantEl.querySelector('#va-close'); + const statusIcon = assistantEl.querySelector('#va-status-icon'); + const statusText = assistantEl.querySelector('#va-status-text'); + const statusSub = assistantEl.querySelector('#va-status-sub'); + const resultBox = assistantEl.querySelector('#va-result'); + const resultText = assistantEl.querySelector('#va-result-text'); + const resultAction = assistantEl.querySelector('#va-result-action'); + const fabMic = fab.querySelector('.va-fab-mic'); + const fabStop = fab.querySelector('.va-fab-stop'); + + // ── Estado ───────────────────────────────────────────────────────────────── + let state = 'idle'; // idle | recording | processing + let recorder = null; + let panelOpen = false; + + function setStatus(s, text, sub = '') { + state = s; + statusText.textContent = text; + statusSub.textContent = sub; + statusIcon.className = `va-status-icon va-status-icon--${s}`; + fab.className = `va-fab va-fab--${s}`; + fabMic.style.display = (s === 'recording') ? 'none' : ''; + fabStop.style.display = (s === 'recording') ? '' : 'none'; + } + + function showResult(heard, actionLabel) { + resultBox.hidden = false; + resultText.textContent = `"${heard}"`; + resultAction.textContent = actionLabel ? `→ ${actionLabel}` : '→ Comando no reconocido'; + resultAction.className = 'va-result-action' + (actionLabel ? '' : ' va-result-action--unknown'); + } + + function openPanel() { + panel.hidden = false; + panelOpen = true; + resultBox.hidden = true; + fab.setAttribute('aria-label', 'Hablar'); + } + + function closePanel() { + panel.hidden = true; + panelOpen = false; + if (state === 'recording' && recorder) { + recorder.stop().catch(() => {}); + recorder = null; + } + setStatus('idle', 'Di un comando…'); + } + + // ── Lógica principal ─────────────────────────────────────────────────────── + fab.addEventListener('click', async () => { + if (!panelOpen) { openPanel(); return; } + + if (state === 'recording') { + // Detener grabación + setStatus('processing', 'Procesando audio…'); + const blob = await recorder.stop(); + recorder = null; + await transcribe(blob); + return; + } + + if (state !== 'idle') return; + + // Iniciar grabación + try { + recorder = await startRecording(); + setStatus('recording', 'Escuchando…', 'Clic para detener'); + resultBox.hidden = true; + // Pre-carga el modelo mientras graba (sin bloquear) + if (!transcriberPromise) loadModel(); + } catch (e) { + console.error('[VA] Micrófono:', e); + setStatus('idle', 'Sin acceso al micrófono'); + } + }); + + closeBtn.addEventListener('click', closePanel); + + async function transcribe(blob) { + try { + // Carga el modelo si aún no está listo + setStatus('processing', 'Cargando modelo…', 'Solo la primera vez (~40 MB)'); + const transcriber = await loadModel(prog => { + if (prog.status === 'progress') { + const pct = Math.round(prog.progress ?? 0); + statusSub.textContent = `Descargando: ${pct}%`; + } + }); + + setStatus('processing', 'Transcribiendo…', ''); + + let audioData; + try { + audioData = await blobToFloat32(blob); + } catch (e) { + console.error('[VA] Error decodificando audio:', e); + setStatus('idle', 'Error al leer el audio'); + return; + } + + if (audioData.length < 3200) { // < 0.2s de audio + setStatus('idle', 'Muy corto, inténtalo de nuevo'); + return; + } + + const result = await transcriber(audioData, { language: 'spanish', task: 'transcribe' }); + const text = (result?.text ?? '').trim(); + + if (!text) { + setStatus('idle', 'No se detectó voz'); + return; + } + + const cmd = matchCommand(text); + showResult(text, cmd?.label ?? ''); + setStatus('idle', 'Di un comando…'); + + if (cmd) { + setTimeout(() => { cmd.action(); }, 600); + } + } catch (e) { + console.error('[VA] Error en transcripción:', e?.message ?? e); + const msg = e?.message?.includes('vacío') ? 'Audio vacío, graba más tiempo' + : e?.message?.includes('session') ? 'Error cargando el modelo' + : e?.message?.includes('decodeAudioData') ? 'Formato de audio no soportado' + : 'Error al procesar'; + setStatus('idle', msg); + } + } +} + +export function hideVoiceAssistant() { + if (assistantEl) assistantEl.style.display = 'none'; +} + +export function showVoiceAssistant() { + if (assistantEl) assistantEl.style.display = ''; + else mountVoiceAssistant(); +} diff --git a/src/main.js b/src/main.js index 202f003..cc968f9 100755 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ import './styles/facturas.css' import './styles/clients.css' import './styles/create-invoice.css' import './styles/settings.css' +import './styles/voice-assistant.css' import { initRouter } from './router.js' import { initSessionManager } from './services/session.js' import { initTheme } from './services/theme.js' diff --git a/src/router.js b/src/router.js index 77a0993..61428c6 100755 --- a/src/router.js +++ b/src/router.js @@ -4,6 +4,7 @@ import { createSidebar } from './components/Sidebar.js'; import { getAvailablePages, getPageByRoute } from './pages/pagesRegistry.js'; import { showConfirmExitModal } from './components/ConfirmExitModal.js'; import { applyLoginTheme, applySavedTheme } from './services/theme.js'; +import { hideVoiceAssistant, showVoiceAssistant } from './components/VoiceAssistant.js'; // 🔧 Modo DEV: cambiar a false para activar login const DEV_MODE = false; @@ -80,8 +81,10 @@ export function initRouter() { if (hash === '#login') { applyLoginTheme(); + hideVoiceAssistant(); } else { applySavedTheme(); + showVoiceAssistant(); } app.innerHTML = ''; @@ -118,12 +121,47 @@ export function initRouter() { // Render page content using registry const pageContent = page.render(); - if (pageContent && typeof pageContent.cleanup === 'function') { - currentPageCleanup = pageContent.cleanup; - } + // Mobile sidebar: backdrop + topbar + const sidebarBackdrop = document.createElement('div'); + sidebarBackdrop.className = 'sidebar-backdrop'; + const openMobileSidebar = () => { + sidebar.classList.add('mobile-open'); + sidebarBackdrop.classList.add('visible'); + }; + const closeMobileSidebar = () => { + sidebar.classList.remove('mobile-open'); + sidebarBackdrop.classList.remove('visible'); + }; + const handleMobileClose = () => closeMobileSidebar(); + document.addEventListener('sidebar:close-mobile', handleMobileClose); + sidebarBackdrop.addEventListener('click', closeMobileSidebar); + + const mobileTopbar = document.createElement('header'); + mobileTopbar.className = 'mobile-topbar'; + mobileTopbar.innerHTML = /*html*/` + + ${page.name} + `; + mobileTopbar.querySelector('.mobile-menu-btn').addEventListener('click', openMobileSidebar); + + // Chain page cleanup with sidebar event listener cleanup + const pageCleanup = pageContent && typeof pageContent.cleanup === 'function' ? pageContent.cleanup : null; + currentPageCleanup = () => { + document.removeEventListener('sidebar:close-mobile', handleMobileClose); + if (pageCleanup) pageCleanup(); + }; + + mainContent.appendChild(mobileTopbar); mainContent.appendChild(pageContent); layout.appendChild(sidebar); + layout.appendChild(sidebarBackdrop); layout.appendChild(mainContent); app.appendChild(layout); } else { diff --git a/src/services/voiceInput.js b/src/services/voiceInput.js new file mode 100644 index 0000000..077375b --- /dev/null +++ b/src/services/voiceInput.js @@ -0,0 +1,190 @@ +import { pipeline } from '@huggingface/transformers'; + +// ── Iconos ──────────────────────────────────────────────────────────────────── + +const MIC_ICON = /*html*/` + + + + + + `; + +const STOP_ICON = /*html*/` + + + `; + +const SPIN_ICON = /*html*/` + + + `; + +// ── Modelo (singleton) ──────────────────────────────────────────────────────── + +let transcriberPromise = null; + +function loadModel(onProgress) { + if (!transcriberPromise) { + transcriberPromise = pipeline( + 'automatic-speech-recognition', + 'Xenova/whisper-tiny', + { progress_callback: onProgress } + ).catch(err => { + transcriberPromise = null; // permitir reintento + throw err; + }); + } + return transcriberPromise; +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function showTip(btn, text, duration = 3000) { + btn.querySelectorAll('.voice-tip').forEach(t => t.remove()); + const tip = document.createElement('span'); + tip.className = 'voice-tip'; + tip.textContent = text; + btn.appendChild(tip); + if (duration > 0) setTimeout(() => tip.remove(), duration); + return tip; +} + +async function startMediaRecorder() { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); + const mimeType = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus'] + .find(t => MediaRecorder.isTypeSupported(t)) || ''; + + const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : {}); + const chunks = []; + recorder.ondataavailable = e => { if (e.data?.size > 0) chunks.push(e.data); }; + recorder.start(200); + + return { + stop: () => new Promise(resolve => { + recorder.onstop = () => { + stream.getTracks().forEach(t => t.stop()); + resolve(new Blob(chunks, { type: mimeType || 'audio/webm' })); + }; + if (recorder.state !== 'inactive') recorder.stop(); + else resolve(new Blob(chunks)); + }) + }; +} + +async function blobToFloat32(blob) { + const arrayBuffer = await blob.arrayBuffer(); + const ctx = new AudioContext({ sampleRate: 16000 }); + const audioBuffer = await ctx.decodeAudioData(arrayBuffer); + await ctx.close(); + return audioBuffer.getChannelData(0); // mono Float32Array a 16kHz +} + +// ── API pública ─────────────────────────────────────────────────────────────── + +/** + * Adjunta un botón de voz (Whisper local) a un input. + * Devuelve el botón, o null si el navegador no soporta MediaRecorder. + */ +export function attachVoiceInput(inputEl, { lang = 'es' } = {}) { + if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) { + console.warn('[VoiceInput] MediaRecorder no disponible en este navegador.'); + return null; + } + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'voice-btn'; + btn.setAttribute('aria-label', 'Buscar por voz'); + btn.title = 'Buscar por voz (Whisper local)'; + btn.innerHTML = MIC_ICON; + + let mediaRecorder = null; // { stop() } + let state = 'idle'; // idle | recording | processing + + const setState = (s) => { + state = s; + btn.className = `voice-btn voice-btn--${s}`; + if (s === 'idle') { btn.innerHTML = MIC_ICON; btn.disabled = false; } + if (s === 'recording') { btn.innerHTML = STOP_ICON; btn.disabled = false; } + if (s === 'processing') { btn.innerHTML = SPIN_ICON; btn.disabled = true; } + }; + + btn.addEventListener('click', async () => { + // ── Detener grabación ────────────────────────────────────────────────── + if (state === 'recording') { + showTip(btn, 'Procesando…', 0); + setState('processing'); + const blob = await mediaRecorder.stop(); + mediaRecorder = null; + await transcribe(blob); + return; + } + + if (state !== 'idle') return; + + // ── Iniciar grabación ────────────────────────────────────────────────── + try { + mediaRecorder = await startMediaRecorder(); + setState('recording'); + showTip(btn, 'Hablando… clic para detener'); + + // Pre-carga el modelo en paralelo mientras el usuario habla + if (!transcriberPromise) loadModel(); + } catch (e) { + console.error('[VoiceInput] Error al acceder al micrófono:', e); + showTip(btn, 'Sin acceso al micrófono'); + setState('idle'); + } + }); + + async function transcribe(blob) { + try { + // Carga / espera el modelo (primera vez descarga ~40 MB, se cachea) + const tipLoad = showTip(btn, 'Cargando modelo…', 0); + const transcriber = await loadModel(progress => { + if (progress.status === 'progress') { + const pct = Math.round(progress.progress ?? 0); + btn.title = `Cargando modelo: ${pct}%`; + } + }); + tipLoad.remove(); + + showTip(btn, 'Transcribiendo…', 0); + const audioData = await blobToFloat32(blob); + const result = await transcriber(audioData, { language: lang, task: 'transcribe' }); + const text = (result.text ?? '').trim(); + + if (text) { + inputEl.value = text; + inputEl.dispatchEvent(new Event('input', { bubbles: true })); + inputEl.focus(); + showTip(btn, `"${text}"`); + } else { + showTip(btn, 'No se detectó texto'); + } + } catch (e) { + console.error('[VoiceInput] Error en transcripción:', e); + showTip(btn, 'Error al transcribir'); + } finally { + btn.title = 'Buscar por voz'; + setState('idle'); + } + } + + return btn; +} + +/** + * Envuelve el input en .search-wrapper y adjunta el botón de voz. + */ +export function wrapWithVoice(inputEl, options = {}) { + const voiceBtn = attachVoiceInput(inputEl, options); + if (!voiceBtn) return; + + const wrapper = document.createElement('div'); + wrapper.className = 'search-wrapper'; + inputEl.parentNode.insertBefore(wrapper, inputEl); + wrapper.appendChild(inputEl); + wrapper.appendChild(voiceBtn); +} diff --git a/src/style.css b/src/style.css index ba5ac09..4e1a9e0 100755 --- a/src/style.css +++ b/src/style.css @@ -333,6 +333,97 @@ button:disabled { } +/* ============================================ + Voice Input + ============================================ */ +.search-wrapper { + position: relative; + display: flex; + align-items: center; + flex: 1; + max-width: 320px; +} + +.search-wrapper .search-input { + width: 100%; + max-width: 100%; + flex: none; + padding-right: 34px; +} + +.clientes-filters .search-wrapper { + max-width: 400px; +} + +.voice-btn { + position: absolute; + right: 6px; + background: none; + border: none; + padding: 4px; + border-radius: var(--radius-sm); + cursor: pointer; + color: var(--gray-400); + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + transition: color var(--transition-fast), background-color var(--transition-fast); + flex-shrink: 0; +} + +.voice-btn:hover:not(:disabled) { + color: var(--primary); + background: var(--blue-50); + border-color: transparent; +} + +.voice-btn:disabled { + cursor: default; + opacity: 0.7; +} + +/* Estado: grabando */ +.voice-btn--recording { + color: var(--red-500); + animation: voice-pulse 1.1s ease-in-out infinite; +} + +/* Estado: procesando */ +.voice-btn--processing .voice-spin { + transform-origin: center; + animation: voice-spin 0.8s linear infinite; +} + +@keyframes voice-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(1.25); } +} + +@keyframes voice-spin { + to { transform: rotate(360deg); } +} + +/* Tooltip flotante del botón de voz */ +.voice-tip { + position: absolute; + bottom: calc(100% + 7px); + left: 50%; + transform: translateX(-50%); + background: var(--gray-900); + color: #fff; + font-size: 11px; + white-space: nowrap; + padding: 3px 8px; + border-radius: 4px; + pointer-events: none; + z-index: 9999; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; +} + .logout-button { background-color: transparent; color: var(--text-secondary); diff --git a/src/styles/facturas.css b/src/styles/facturas.css index 637b210..74d2156 100644 --- a/src/styles/facturas.css +++ b/src/styles/facturas.css @@ -236,6 +236,12 @@ background-color: rgba(30, 58, 110, 0.24); } +@media (max-width: 1024px) { + .facturas-page { + padding: var(--space-4); + } +} + @media (max-width: 768px) { .facturas-filters { flex-direction: column; @@ -263,8 +269,26 @@ } } -@media (max-width: 1024px) { +@media (max-width: 640px) { .facturas-page { - padding: var(--space-4); + padding: var(--space-3); + } + + /* Scroll horizontal suave en la tabla */ + .invoices-table-container { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .invoices-table { + min-width: 560px; + } + + /* Ocultar columnas menos importantes en pantallas pequeñas */ + .invoices-table th.invoice-date, + .invoices-table td.invoice-date, + .invoices-table th.invoice-remain, + .invoices-table td.invoice-remain { + display: none; } } diff --git a/src/styles/login.css b/src/styles/login.css index d7140d1..a33b989 100644 --- a/src/styles/login.css +++ b/src/styles/login.css @@ -8,27 +8,60 @@ align-items: center; min-height: 100vh; width: 100%; - background: var(--bg-page); + background: linear-gradient(145deg, var(--bg-page) 0%, var(--blue-50) 100%); } .login-card { background: var(--card-bg); padding: var(--space-8); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-sm); + border-radius: 12px; + box-shadow: var(--shadow-md); width: 100%; - max-width: 380px; + max-width: 390px; border: 1px solid var(--border-color); } +.login-brand { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + margin-bottom: var(--space-6); +} + +.login-logo-icon { + width: 40px; + height: 40px; + background: var(--primary); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: white; +} + +.login-brand-name { + font-size: 1.25rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.02em; +} + .login-title { color: var(--text-primary); - margin: 0 0 var(--space-6) 0; - font-size: 1.25rem; + margin: 0 0 var(--space-1) 0; + font-size: 1.125rem; text-align: center; font-weight: 600; } +.login-subtitle { + color: var(--text-secondary); + font-size: 13px; + text-align: center; + margin: 0 0 var(--space-5) 0; +} + .form-group { margin-bottom: var(--space-4); } @@ -169,6 +202,11 @@ } @media (max-width: 640px) { + .login-card { + margin: var(--space-4); + padding: var(--space-6); + } + .session-warning-actions { flex-direction: column; } @@ -177,3 +215,7 @@ width: 100%; } } + +:root[data-theme='dark'] .login-container { + background: linear-gradient(145deg, var(--bg-page) 0%, rgba(30, 58, 110, 0.2) 100%); +} diff --git a/src/styles/sidebar.css b/src/styles/sidebar.css index f059221..d887a0e 100644 --- a/src/styles/sidebar.css +++ b/src/styles/sidebar.css @@ -29,6 +29,26 @@ height: 52px; } +.sidebar-brand { + display: flex; + align-items: center; + gap: var(--space-2); + min-width: 0; + overflow: hidden; +} + +.sidebar-logo-icon { + width: 26px; + height: 26px; + background: var(--primary); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + color: white; + flex-shrink: 0; +} + .sidebar-header h2 { margin: 0; font-size: 14px; @@ -137,7 +157,7 @@ .sidebar.collapsed { width: 56px; } -.sidebar.collapsed .sidebar-header h2, +.sidebar.collapsed .sidebar-brand h2, .sidebar.collapsed .sidebar-text { opacity: 0; width: 0; @@ -164,22 +184,110 @@ padding: var(--space-6); } +/* ── Mobile topbar (hidden on desktop) ── */ +.mobile-topbar { + display: none; +} + +/* ── Sidebar backdrop ── */ +.sidebar-backdrop { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 999; + animation: backdropFadeIn 150ms ease; +} + +.sidebar-backdrop.visible { + display: block; +} + +@keyframes backdropFadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + @media (max-width: 768px) { + /* Sidebar: oculta por defecto, se muestra con .mobile-open */ .sidebar { position: fixed; left: 0; top: 0; bottom: 0; z-index: 1000; - transform: translateX(0); + transform: translateX(-100%); + width: 220px !important; + transition: transform var(--transition-base), box-shadow var(--transition-base); + box-shadow: none; } + .sidebar.mobile-open { + transform: translateX(0); + box-shadow: var(--shadow-lg); + } + + /* Anular collapsed en móvil para que el contenido se vea bien cuando se abre */ .sidebar.collapsed { - transform: translateX(-100%); - width: 220px; + width: 220px !important; + } + + .sidebar.collapsed .sidebar-brand h2, + .sidebar.collapsed .sidebar-text { + opacity: 1; + width: auto; + overflow: visible; + } + + .sidebar.collapsed .sidebar-link { + justify-content: flex-start; + padding: var(--space-2) var(--space-3); } .main-content { margin-left: 0; } + + /* Topbar móvil */ + .mobile-topbar { + display: flex; + align-items: center; + gap: var(--space-3); + padding: 0 var(--space-4); + height: 52px; + background: var(--card-bg); + border-bottom: 1px solid var(--border-color); + position: sticky; + top: 0; + z-index: 50; + flex-shrink: 0; + } + + .mobile-menu-btn { + background: transparent; + border: none; + padding: var(--space-2); + border-radius: var(--radius-md); + color: var(--text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + flex-shrink: 0; + transition: background-color var(--transition-fast), color var(--transition-fast); + } + + .mobile-menu-btn:hover { + background: var(--gray-100); + color: var(--text-primary); + border-color: transparent; + } + + .mobile-topbar-title { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + } } diff --git a/src/styles/voice-assistant.css b/src/styles/voice-assistant.css new file mode 100644 index 0000000..b456049 --- /dev/null +++ b/src/styles/voice-assistant.css @@ -0,0 +1,302 @@ +/* ============================================================ + Voice Assistant — Floating button + panel + ============================================================ */ + +.va-root { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 600; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; +} + +/* ── FAB ─────────────────────────────────────────────────── */ +.va-fab { + width: 50px; + height: 50px; + border-radius: 50%; + background: var(--primary); + color: #fff; + border: none; + cursor: pointer; + box-shadow: 0 4px 14px rgba(37, 99, 235, 0.45); + display: flex; + align-items: center; + justify-content: center; + transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast); + flex-shrink: 0; +} + +.va-fab:hover { + background: var(--primary-hover); + box-shadow: 0 6px 18px rgba(37, 99, 235, 0.55); + transform: scale(1.06); + border-color: transparent; +} + +.va-fab--recording { + background: var(--red-500) !important; + box-shadow: 0 4px 14px rgba(239, 68, 68, 0.45) !important; + animation: va-pulse-fab 1.2s ease-in-out infinite; +} + +.va-fab--processing { + background: var(--amber-600) !important; + box-shadow: 0 4px 14px rgba(217, 119, 6, 0.4) !important; +} + +@keyframes va-pulse-fab { + 0%, 100% { box-shadow: 0 4px 14px rgba(239, 68, 68, 0.45); } + 50% { box-shadow: 0 4px 22px rgba(239, 68, 68, 0.75); transform: scale(1.06); } +} + +/* ── Panel ───────────────────────────────────────────────── */ +.va-panel { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 14px; + width: 290px; + box-shadow: var(--shadow-lg); + overflow: hidden; + animation: va-slide-up 0.18s ease; +} + +@keyframes va-slide-up { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.va-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px 10px; + border-bottom: 1px solid var(--border-color); +} + +.va-panel-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.va-close-btn { + background: none; + border: none; + cursor: pointer; + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: var(--radius-sm); + padding: 0; + transition: background-color var(--transition-fast), color var(--transition-fast); +} + +.va-close-btn:hover { + background: var(--gray-100); + color: var(--text-primary); + border-color: transparent; +} + +/* ── Área de estado ──────────────────────────────────────── */ +.va-status-area { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px 16px 16px; + gap: 8px; +} + +.va-status-icon { + width: 44px; + height: 44px; + border-radius: 50%; + background: var(--gray-100); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + transition: background-color var(--transition-fast), color var(--transition-fast); +} + +.va-status-icon--recording { + background: rgba(239, 68, 68, 0.12); + color: var(--red-500); + animation: va-pulse-icon 1.2s ease-in-out infinite; +} + +.va-status-icon--processing { + background: rgba(217, 119, 6, 0.1); + color: var(--amber-600); +} + +@keyframes va-pulse-icon { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.12); } +} + +.va-status-text { + margin: 0; + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + text-align: center; +} + +.va-status-sub { + margin: 0; + font-size: 11px; + color: var(--text-secondary); + text-align: center; + min-height: 14px; +} + +/* ── Resultado ───────────────────────────────────────────── */ +.va-result { + margin: 0 12px 12px; + background: var(--gray-50); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.va-result-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-secondary); +} + +.va-result-text { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); +} + +.va-result-action { + font-size: 12px; + color: var(--green-600); + font-weight: 500; +} + +.va-result-action--unknown { + color: var(--text-secondary); +} + +/* ── Lista de comandos ───────────────────────────────────── */ +.va-commands { + border-top: 1px solid var(--border-color); + padding: 10px 0 4px; +} + +.va-commands-title { + margin: 0 0 6px; + padding: 0 14px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-secondary); +} + +.va-commands-list { + list-style: none; + margin: 0; + padding: 0; +} + +.va-cmd-item { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 14px; + cursor: pointer; + transition: background-color var(--transition-fast); +} + +.va-cmd-item:hover { + background: var(--gray-50); +} + +.va-cmd-icon { + width: 26px; + height: 26px; + background: var(--blue-50); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + color: var(--primary); + flex-shrink: 0; +} + +.va-cmd-info { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; +} + +.va-cmd-label { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); +} + +.va-cmd-hint { + font-size: 11px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ── Dark mode ───────────────────────────────────────────── */ +:root[data-theme='dark'] .va-panel { + background: #081226; + border-color: #1e2f4d; +} + +:root[data-theme='dark'] .va-close-btn:hover, +:root[data-theme='dark'] .va-cmd-item:hover { + background: rgba(30, 58, 110, 0.3); +} + +:root[data-theme='dark'] .va-result { + background: rgba(15, 23, 42, 0.8); + border-color: #1e2f4d; +} + +:root[data-theme='dark'] .va-status-icon { + background: rgba(148, 163, 184, 0.1); +} + +:root[data-theme='dark'] .va-cmd-icon { + background: rgba(30, 58, 110, 0.4); +} + +/* ── Móvil ───────────────────────────────────────────────── */ +@media (max-width: 480px) { + .va-root { + bottom: 16px; + right: 16px; + } + + .va-panel { + width: calc(100vw - 32px); + max-width: 290px; + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..c7f6054 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + optimizeDeps: { + exclude: ['@huggingface/transformers'], + }, +});