Primeiro Commit
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# API Configuration
|
||||
# Base URL da API do HGTX
|
||||
VITE_API_BASE_URL=https://prod-hgtx-intelligence-n8n.hgtx.com.br
|
||||
|
||||
# API Key para autenticação
|
||||
# IMPORTANTE: Mantenha esta chave segura e não compartilhe publicamente
|
||||
VITE_API_KEY=your-api-key-here
|
||||
|
||||
# User Configuration
|
||||
# Configure com os dados do usuário/estabelecimento
|
||||
VITE_USER_EMAIL=user@example.com
|
||||
VITE_ESTABELECIMENTO_ID=1
|
||||
|
||||
# Instruções:
|
||||
# 1. Copie este arquivo para .env
|
||||
# 2. Substitua os valores acima com as configurações reais
|
||||
# 3. NUNCA commite o arquivo .env no git
|
||||
@@ -12,6 +12,11 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
Generated
+3
-113
@@ -83,7 +83,6 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -847,7 +846,6 @@
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
@@ -865,7 +863,6 @@
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
@@ -880,7 +877,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -890,7 +886,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -900,14 +895,12 @@
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -918,7 +911,6 @@
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
@@ -932,7 +924,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -942,7 +933,6 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
@@ -956,7 +946,6 @@
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -2929,14 +2918,14 @@
|
||||
"version": "15.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
|
||||
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -2947,7 +2936,7 @@
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
@@ -3269,7 +3258,6 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -3282,7 +3270,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -3298,14 +3285,12 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
@@ -3319,7 +3304,6 @@
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
@@ -3400,14 +3384,12 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -3431,7 +3413,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -3500,7 +3481,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -3548,7 +3528,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
@@ -3573,7 +3552,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -3622,7 +3600,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -3635,7 +3612,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
@@ -3654,7 +3630,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -3671,7 +3646,6 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -3685,7 +3659,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
@@ -3881,14 +3854,12 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dlv": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
@@ -3919,7 +3890,6 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
@@ -3961,7 +3931,6 @@
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
@@ -4285,7 +4254,6 @@
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@@ -4302,7 +4270,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -4329,7 +4296,6 @@
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -4352,7 +4318,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -4423,7 +4388,6 @@
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.0",
|
||||
@@ -4470,7 +4434,6 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -4540,7 +4503,6 @@
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
@@ -4561,7 +4523,6 @@
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
@@ -4574,7 +4535,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -4584,7 +4544,6 @@
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@@ -4737,7 +4696,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
@@ -4750,7 +4708,6 @@
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
|
||||
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
@@ -4766,7 +4723,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -4776,7 +4732,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -4786,7 +4741,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -4799,7 +4753,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -4809,14 +4762,12 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
@@ -4832,7 +4783,6 @@
|
||||
"version": "1.21.6",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
|
||||
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -4906,7 +4856,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -4919,7 +4868,6 @@
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
@@ -5429,7 +5377,6 @@
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
@@ -5463,7 +5410,6 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -5473,7 +5419,6 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -5521,7 +5466,6 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -5538,7 +5482,6 @@
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0",
|
||||
@@ -5550,7 +5493,6 @@
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -5593,7 +5535,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -5622,7 +5563,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -5682,7 +5622,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
@@ -5712,7 +5651,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -5722,14 +5660,12 @@
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
@@ -5746,14 +5682,12 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -5766,7 +5700,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -5776,7 +5709,6 @@
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
||||
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -5786,7 +5718,6 @@
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -5815,7 +5746,6 @@
|
||||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-value-parser": "^4.0.0",
|
||||
@@ -5833,7 +5763,6 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
|
||||
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camelcase-css": "^2.0.1"
|
||||
@@ -5853,7 +5782,6 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
|
||||
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -5889,7 +5817,6 @@
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -5915,7 +5842,6 @@
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
@@ -5929,7 +5855,6 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
@@ -5979,7 +5904,6 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -6203,7 +6127,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pify": "^2.3.0"
|
||||
@@ -6213,7 +6136,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
@@ -6258,7 +6180,6 @@
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.13.0",
|
||||
@@ -6286,7 +6207,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
@@ -6333,7 +6253,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -6379,7 +6298,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -6392,7 +6310,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6402,7 +6319,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -6425,7 +6341,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -6435,7 +6350,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
@@ -6454,7 +6368,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -6469,7 +6382,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6479,14 +6391,12 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -6499,7 +6409,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
@@ -6516,7 +6425,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -6529,7 +6437,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6552,7 +6459,6 @@
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
@@ -6588,7 +6494,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -6611,7 +6516,6 @@
|
||||
"version": "3.4.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -6658,7 +6562,6 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0"
|
||||
@@ -6668,7 +6571,6 @@
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
@@ -6687,7 +6589,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@@ -6713,7 +6614,6 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
@@ -6877,7 +6777,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vaul": {
|
||||
@@ -6979,7 +6878,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@@ -7005,7 +6903,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
@@ -7024,7 +6921,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -7042,7 +6938,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -7052,14 +6947,12 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -7074,7 +6967,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -7087,7 +6979,6 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -7100,7 +6991,6 @@
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
|
||||
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
|
||||
@@ -25,21 +25,28 @@ interface ChatHeaderProps {
|
||||
onSystemPromptChange?: (prompt: string) => void;
|
||||
}
|
||||
|
||||
export const ChatHeader = ({
|
||||
export const ChatHeader = ({
|
||||
showModelSelector = false,
|
||||
selectedModel = "ChatGPT 4.1",
|
||||
selectedModel = "GPT-4o",
|
||||
onModelChange,
|
||||
systemPrompt = "Você é um assistente útil e prestativo.",
|
||||
onSystemPromptChange
|
||||
}: ChatHeaderProps) => {
|
||||
const [isPersonalityOpen, setIsPersonalityOpen] = useState(false);
|
||||
const [tempSystemPrompt, setTempSystemPrompt] = useState(systemPrompt);
|
||||
|
||||
|
||||
const models = [
|
||||
"ChatGPT 4.1",
|
||||
"ChatGPT 5",
|
||||
"GPT-4.1",
|
||||
"GPT-4o",
|
||||
"GPT-5 Mini",
|
||||
"Gemini 2.0 Flash",
|
||||
"Claude Sonnet 4.5",
|
||||
"Claude Opus 4.1",
|
||||
"DeepSeek V3.2 Chat",
|
||||
"DeepSeek V3.2 Reasoner",
|
||||
"Gemini 2.5 Flash",
|
||||
"Gemini 2.5 Flash-Lite",
|
||||
"Claude Haiku 4.5",
|
||||
"GPT-4o Mini",
|
||||
];
|
||||
|
||||
const handleSavePersonality = () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { audioGenerationService, VOICE_OPTIONS, VoiceType } from "@/services/audioGeneration";
|
||||
|
||||
interface GeneratedAudio {
|
||||
id: string;
|
||||
@@ -24,49 +25,10 @@ interface GeneratedAudio {
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
const VOICE_OPTIONS = {
|
||||
alloy: {
|
||||
label: "Alloy",
|
||||
gender: "Masculina",
|
||||
style: "Neutra, equilibrada, tom corporativo",
|
||||
description: "Boa para tutoriais e comunicações institucionais."
|
||||
},
|
||||
echo: {
|
||||
label: "Echo",
|
||||
gender: "Masculina",
|
||||
style: "Forte e profissional, mais grave",
|
||||
description: "Ideal para voz de autoridade ou locução firme."
|
||||
},
|
||||
fable: {
|
||||
label: "Fable",
|
||||
gender: "Feminina",
|
||||
style: "Narrativa, calorosa e envolvente",
|
||||
description: "Ótima para storytelling e áudios empáticos."
|
||||
},
|
||||
onyx: {
|
||||
label: "Onyx",
|
||||
gender: "Masculina",
|
||||
style: "Grave, autoritária, impactante",
|
||||
description: "Excelente para trailers, mensagens sérias ou institucionais."
|
||||
},
|
||||
nova: {
|
||||
label: "Nova",
|
||||
gender: "Feminina",
|
||||
style: "Brilhante, animada, energética",
|
||||
description: "Boa para vídeos curtos, marketing ou conteúdos leves."
|
||||
},
|
||||
shimmer: {
|
||||
label: "Shimmer",
|
||||
gender: "Feminina",
|
||||
style: "Suave, otimista, clara",
|
||||
description: "Boa para mensagens acolhedoras, explicações e IA conversacional."
|
||||
}
|
||||
};
|
||||
|
||||
export const GenerationView = () => {
|
||||
const [textToSpeech, setTextToSpeech] = useState("");
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [selectedVoice, setSelectedVoice] = useState<keyof typeof VOICE_OPTIONS>("alloy");
|
||||
const [selectedVoice, setSelectedVoice] = useState<VoiceType>("alloy");
|
||||
const [generatedAudio, setGeneratedAudio] = useState<GeneratedAudio | null>(null);
|
||||
const [audioHistory, setAudioHistory] = useState<GeneratedAudio[]>([]);
|
||||
const [audioSearchQuery, setAudioSearchQuery] = useState("");
|
||||
@@ -79,32 +41,67 @@ export const GenerationView = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleGenerateAudio = () => {
|
||||
setIsProcessing(true);
|
||||
|
||||
setTimeout(() => {
|
||||
const audio: GeneratedAudio = {
|
||||
id: Date.now().toString(),
|
||||
text: textToSpeech,
|
||||
voice: selectedVoice,
|
||||
voiceLabel: VOICE_OPTIONS[selectedVoice].label,
|
||||
audioUrl: "data:audio/mp3;base64,//sample",
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setGeneratedAudio(audio);
|
||||
|
||||
const newHistory = [audio, ...audioHistory].slice(0, 10);
|
||||
setAudioHistory(newHistory);
|
||||
localStorage.setItem('audioHistory', JSON.stringify(newHistory));
|
||||
|
||||
setIsProcessing(false);
|
||||
|
||||
const handleGenerateAudio = async () => {
|
||||
// Valida o texto antes de enviar
|
||||
const validation = audioGenerationService.validateText(textToSpeech);
|
||||
if (!validation.valid) {
|
||||
toast({
|
||||
title: "Áudio gerado com sucesso",
|
||||
description: `Voz: ${VOICE_OPTIONS[selectedVoice].label}`,
|
||||
title: "Texto inválido",
|
||||
description: validation.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
// Chama o serviço de geração de áudio
|
||||
const response = await audioGenerationService.generateAudio({
|
||||
message: textToSpeech,
|
||||
voice: selectedVoice,
|
||||
});
|
||||
|
||||
// Verifica se a geração foi bem-sucedida
|
||||
if (response.success) {
|
||||
console.log('URL do áudio gerado:', response.audio_url);
|
||||
|
||||
const audio: GeneratedAudio = {
|
||||
id: response.audio_generation_id,
|
||||
text: response.message,
|
||||
voice: selectedVoice,
|
||||
voiceLabel: VOICE_OPTIONS[selectedVoice].label,
|
||||
audioUrl: response.audio_url,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setGeneratedAudio(audio);
|
||||
|
||||
const newHistory = [audio, ...audioHistory].slice(0, 10);
|
||||
setAudioHistory(newHistory);
|
||||
localStorage.setItem('audioHistory', JSON.stringify(newHistory));
|
||||
|
||||
toast({
|
||||
title: "Áudio gerado com sucesso",
|
||||
description: `Voz: ${VOICE_OPTIONS[selectedVoice].label}`,
|
||||
});
|
||||
|
||||
// Limpa o campo de texto após sucesso
|
||||
setTextToSpeech("");
|
||||
} else {
|
||||
throw new Error('Erro ao gerar áudio');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Erro na geração de áudio:', error);
|
||||
|
||||
toast({
|
||||
title: "Erro na geração",
|
||||
description: error.message || "Não foi possível gerar o áudio. Tente novamente.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadAudio = (audio: GeneratedAudio) => {
|
||||
@@ -177,7 +174,7 @@ export const GenerationView = () => {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo de Voz</label>
|
||||
<Select value={selectedVoice} onValueChange={(value) => setSelectedVoice(value as keyof typeof VOICE_OPTIONS)}>
|
||||
<Select value={selectedVoice} onValueChange={(value) => setSelectedVoice(value as VoiceType)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -258,7 +255,12 @@ export const GenerationView = () => {
|
||||
|
||||
<div className="bg-muted/30 rounded-lg p-4">
|
||||
<p className="text-sm mb-3">{generatedAudio.text}</p>
|
||||
<audio controls className="w-full">
|
||||
<audio
|
||||
key={generatedAudio.id}
|
||||
controls
|
||||
className="w-full"
|
||||
preload="metadata"
|
||||
>
|
||||
<source src={generatedAudio.audioUrl} type="audio/mpeg" />
|
||||
Seu navegador não suporta o elemento de áudio.
|
||||
</audio>
|
||||
@@ -325,7 +327,12 @@ export const GenerationView = () => {
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{audio.text}
|
||||
</p>
|
||||
<audio controls className="w-full">
|
||||
<audio
|
||||
key={audio.id}
|
||||
controls
|
||||
className="w-full"
|
||||
preload="metadata"
|
||||
>
|
||||
<source src={audio.audioUrl} type="audio/mpeg" />
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Upload, Download, Trash2, FileAudio, Copy, Search } from "lucide-react"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { transcriptionService } from "@/services/transcription";
|
||||
|
||||
const SUPPORTED_FORMATS = ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm'];
|
||||
const MAX_FILE_SIZE = 25 * 1024 * 1024; // 25 MB
|
||||
@@ -15,6 +16,7 @@ interface TranscriptionResult {
|
||||
fileName: string;
|
||||
text: string;
|
||||
timestamp: Date;
|
||||
audioUrl?: string;
|
||||
}
|
||||
|
||||
export const TranscriptionView = () => {
|
||||
@@ -36,20 +38,12 @@ export const TranscriptionView = () => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
// Valida o arquivo usando o serviço
|
||||
const validation = transcriptionService.validateAudioFile(file);
|
||||
if (!validation.valid) {
|
||||
toast({
|
||||
title: "Arquivo muito grande",
|
||||
description: "O arquivo deve ter no máximo 25 MB",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const fileExtension = file.name.split('.').pop()?.toLowerCase();
|
||||
if (!fileExtension || !SUPPORTED_FORMATS.includes(fileExtension)) {
|
||||
toast({
|
||||
title: "Formato não suportado",
|
||||
description: `Formatos suportados: ${SUPPORTED_FORMATS.join(', ')}`,
|
||||
title: "Arquivo inválido",
|
||||
description: validation.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -61,28 +55,47 @@ export const TranscriptionView = () => {
|
||||
|
||||
const handleTranscription = async (file: File) => {
|
||||
setIsTranscribing(true);
|
||||
|
||||
setTimeout(() => {
|
||||
const result: TranscriptionResult = {
|
||||
id: Date.now().toString(),
|
||||
fileName: file.name,
|
||||
text: "Esta é uma transcrição de exemplo do áudio enviado. O sistema utilizará modelos de IA avançados para converter sua fala em texto com alta precisão. O áudio foi processado com sucesso e convertido para texto.",
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setTranscriptionResult(result);
|
||||
|
||||
const newHistory = [result, ...transcriptionHistory].slice(0, 10);
|
||||
setTranscriptionHistory(newHistory);
|
||||
localStorage.setItem('transcriptionHistory', JSON.stringify(newHistory));
|
||||
|
||||
setIsTranscribing(false);
|
||||
|
||||
toast({
|
||||
title: "Transcrição concluída",
|
||||
description: "Seu áudio foi transcrito com sucesso!",
|
||||
|
||||
try {
|
||||
// Chama o serviço de transcrição
|
||||
const response = await transcriptionService.transcribeAudio({
|
||||
audioFile: file,
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
// Verifica se a transcrição foi bem-sucedida
|
||||
if (response.success) {
|
||||
const result: TranscriptionResult = {
|
||||
id: response.transcription_id,
|
||||
fileName: file.name,
|
||||
text: response.message,
|
||||
timestamp: new Date(),
|
||||
audioUrl: response.audio_url,
|
||||
};
|
||||
|
||||
setTranscriptionResult(result);
|
||||
|
||||
const newHistory = [result, ...transcriptionHistory].slice(0, 10);
|
||||
setTranscriptionHistory(newHistory);
|
||||
localStorage.setItem('transcriptionHistory', JSON.stringify(newHistory));
|
||||
|
||||
toast({
|
||||
title: "Transcrição concluída",
|
||||
description: "Seu áudio foi transcrito com sucesso!",
|
||||
});
|
||||
} else {
|
||||
throw new Error(response.message || 'Erro ao transcrever áudio');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Erro na transcrição:', error);
|
||||
|
||||
toast({
|
||||
title: "Erro na transcrição",
|
||||
description: error.message || "Não foi possível transcrever o áudio. Tente novamente.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsTranscribing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTranscription = () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { chatService } from "@/services/chat";
|
||||
|
||||
interface ChatInputProps {
|
||||
onSendMessage: (message: string, files?: File[]) => void;
|
||||
@@ -67,21 +68,54 @@ export const ChatInput = ({ onSendMessage }: ChatInputProps) => {
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
|
||||
// Validate file size (max 10MB per file)
|
||||
const validFiles = files.filter((file) => {
|
||||
const newFiles = [...attachedFiles, ...files];
|
||||
|
||||
// Valida quantidade máxima de anexos (5)
|
||||
if (newFiles.length > chatService.getMaxAttachments()) {
|
||||
toast({
|
||||
title: "Limite de anexos excedido",
|
||||
description: `Você pode anexar no máximo ${chatService.getMaxAttachments()} arquivos por mensagem.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Valida tipos de arquivo e tamanho
|
||||
const validFiles: File[] = [];
|
||||
for (const file of files) {
|
||||
// Valida tamanho (max 10MB por arquivo)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
toast({
|
||||
title: "Arquivo muito grande",
|
||||
description: `${file.name} excede o limite de 10MB`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
setAttachedFiles([...attachedFiles, ...validFiles]);
|
||||
validFiles.push(file);
|
||||
}
|
||||
|
||||
// Valida formatos permitidos
|
||||
const allFiles = [...attachedFiles, ...validFiles];
|
||||
const validation = chatService.validateAttachments(allFiles);
|
||||
|
||||
if (!validation.valid) {
|
||||
toast({
|
||||
title: "Formato de arquivo não permitido",
|
||||
description: validation.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setAttachedFiles(allFiles);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
@@ -156,7 +190,8 @@ export const ChatInput = ({ onSendMessage }: ChatInputProps) => {
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
accept=".pdf,.txt,.doc,.docx,.xls,.xlsx,.csv,.png,.jpg,.jpeg,.gif,.webp"
|
||||
accept={chatService.getAllowedFileExtensions()}
|
||||
title={`Formatos aceitos: ${chatService.getAllowedFileTypesLabel()}`}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -179,7 +214,7 @@ export const ChatInput = ({ onSendMessage }: ChatInputProps) => {
|
||||
</div>
|
||||
|
||||
<p className="hidden md:block text-xs text-muted-foreground text-center">
|
||||
Pressione Enter para enviar, Shift + Enter para nova linha • Máx. 10MB por arquivo
|
||||
Pressione Enter para enviar, Shift + Enter para nova linha • Máx. {chatService.getMaxAttachments()} anexos • Formatos: {chatService.getAllowedFileTypesLabel()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+300
-134
@@ -1,8 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Search, Folder, FolderPlus, MessageSquare, MoreVertical, Trash2, Edit, FolderInput, ChevronLeft, ChevronRight, Plus } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { chatService, StoredChat, StoredFolder } from "@/services/chat";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -30,111 +34,153 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
interface Conversation {
|
||||
id: string;
|
||||
title: string;
|
||||
lastMessage: string;
|
||||
timestamp: string;
|
||||
folderId?: string;
|
||||
}
|
||||
|
||||
interface ChatFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ChatSidebarProps {
|
||||
isCollapsed: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
onNewChat: () => void;
|
||||
onSelectChat?: (chat: StoredChat) => void;
|
||||
currentChatId?: string;
|
||||
}
|
||||
|
||||
export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat }: ChatSidebarProps) => {
|
||||
export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelectChat, currentChatId }: ChatSidebarProps) => {
|
||||
const { toast } = useToast();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
|
||||
const [isEditFolderOpen, setIsEditFolderOpen] = useState(false);
|
||||
const [isDeleteFolderOpen, setIsDeleteFolderOpen] = useState(false);
|
||||
const [isDeleteChatOpen, setIsDeleteChatOpen] = useState(false);
|
||||
const [newFolderName, setNewFolderName] = useState("");
|
||||
const [editingFolder, setEditingFolder] = useState<ChatFolder | null>(null);
|
||||
const [deletingFolder, setDeletingFolder] = useState<ChatFolder | null>(null);
|
||||
const [selectedConversation, setSelectedConversation] = useState<string | null>("1");
|
||||
const [editingFolder, setEditingFolder] = useState<StoredFolder | null>(null);
|
||||
const [deletingFolder, setDeletingFolder] = useState<StoredFolder | null>(null);
|
||||
const [deletingChat, setDeletingChat] = useState<StoredChat | null>(null);
|
||||
|
||||
const [folders, setFolders] = useState<ChatFolder[]>([
|
||||
{ id: "work", name: "Trabalho" },
|
||||
{ id: "personal", name: "Pessoal" },
|
||||
]);
|
||||
// Estado carregado do localStorage via chatService
|
||||
const [chats, setChats] = useState<StoredChat[]>([]);
|
||||
const [folders, setFolders] = useState<StoredFolder[]>([]);
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
|
||||
const [conversations, setConversations] = useState<Conversation[]>([
|
||||
{
|
||||
id: "1",
|
||||
title: "Conversa sobre IA",
|
||||
lastMessage: "Como funciona o machine learning?",
|
||||
timestamp: "Há 5 min",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Projeto HGTX",
|
||||
lastMessage: "Discutindo arquitetura do sistema",
|
||||
timestamp: "Há 1 hora",
|
||||
folderId: "work",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Ideias criativas",
|
||||
lastMessage: "Brainstorm para novo produto",
|
||||
timestamp: "Ontem",
|
||||
folderId: "personal",
|
||||
},
|
||||
]);
|
||||
// Carrega chats e pastas do localStorage quando o componente monta
|
||||
useEffect(() => {
|
||||
loadChatsAndFolders();
|
||||
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(["work", "personal"]));
|
||||
// Escuta evento customizado para recarregar quando chat é atualizado
|
||||
const handleChatUpdate = () => {
|
||||
loadChatsAndFolders();
|
||||
};
|
||||
|
||||
window.addEventListener('chatUpdated', handleChatUpdate);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('chatUpdated', handleChatUpdate);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadChatsAndFolders = () => {
|
||||
const loadedChats = chatService.getAllChats();
|
||||
const loadedFolders = chatService.getAllFolders();
|
||||
|
||||
setChats(loadedChats);
|
||||
setFolders(loadedFolders);
|
||||
|
||||
// Expande todas as pastas por padrão
|
||||
setExpandedFolders(new Set(loadedFolders.map(f => f.id)));
|
||||
};
|
||||
|
||||
const handleCreateFolder = () => {
|
||||
if (newFolderName.trim()) {
|
||||
const newFolder: ChatFolder = {
|
||||
id: Date.now().toString(),
|
||||
name: newFolderName,
|
||||
};
|
||||
setFolders([...folders, newFolder]);
|
||||
setNewFolderName("");
|
||||
setIsCreateFolderOpen(false);
|
||||
try {
|
||||
const newFolder: StoredFolder = {
|
||||
id: chatService.generateChatId(), // Usa mesmo gerador de ID
|
||||
name: newFolderName,
|
||||
createdAt: new Date(),
|
||||
chatIds: [],
|
||||
};
|
||||
chatService.saveFolder(newFolder);
|
||||
loadChatsAndFolders();
|
||||
setNewFolderName("");
|
||||
setIsCreateFolderOpen(false);
|
||||
|
||||
toast({
|
||||
title: "Pasta criada",
|
||||
description: `Pasta "${newFolderName}" criada com sucesso.`,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erro ao criar pasta",
|
||||
description: "Não foi possível criar a pasta. Tente novamente.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditFolder = () => {
|
||||
if (editingFolder && newFolderName.trim()) {
|
||||
setFolders(
|
||||
folders.map((f) =>
|
||||
f.id === editingFolder.id ? { ...f, name: newFolderName } : f
|
||||
)
|
||||
);
|
||||
setNewFolderName("");
|
||||
setEditingFolder(null);
|
||||
setIsEditFolderOpen(false);
|
||||
try {
|
||||
const updatedFolder: StoredFolder = {
|
||||
...editingFolder,
|
||||
name: newFolderName,
|
||||
};
|
||||
chatService.saveFolder(updatedFolder);
|
||||
loadChatsAndFolders();
|
||||
setNewFolderName("");
|
||||
setEditingFolder(null);
|
||||
setIsEditFolderOpen(false);
|
||||
|
||||
toast({
|
||||
title: "Pasta renomeada",
|
||||
description: `Pasta renomeada para "${newFolderName}".`,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erro ao renomear pasta",
|
||||
description: "Não foi possível renomear a pasta. Tente novamente.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFolder = () => {
|
||||
if (deletingFolder) {
|
||||
// Remove folder and move conversations back to "no folder"
|
||||
setConversations(
|
||||
conversations.map((conv) =>
|
||||
conv.folderId === deletingFolder.id ? { ...conv, folderId: undefined } : conv
|
||||
)
|
||||
);
|
||||
setFolders(folders.filter((f) => f.id !== deletingFolder.id));
|
||||
setDeletingFolder(null);
|
||||
setIsDeleteFolderOpen(false);
|
||||
try {
|
||||
// Remove chats da pasta (volta para "Sem Pasta")
|
||||
const updatedChats = chats.map(chat => {
|
||||
if (chat.folderId === deletingFolder.id) {
|
||||
const updated = { ...chat, folderId: undefined };
|
||||
chatService.saveChat(updated);
|
||||
return updated;
|
||||
}
|
||||
return chat;
|
||||
});
|
||||
|
||||
chatService.deleteFolder(deletingFolder.id);
|
||||
loadChatsAndFolders();
|
||||
setDeletingFolder(null);
|
||||
setIsDeleteFolderOpen(false);
|
||||
|
||||
toast({
|
||||
title: "Pasta excluída",
|
||||
description: "As conversas foram movidas para 'Sem Pasta'.",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erro ao excluir pasta",
|
||||
description: "Não foi possível excluir a pasta. Tente novamente.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openEditFolder = (folder: ChatFolder) => {
|
||||
const openEditFolder = (folder: StoredFolder) => {
|
||||
setEditingFolder(folder);
|
||||
setNewFolderName(folder.name);
|
||||
setIsEditFolderOpen(true);
|
||||
};
|
||||
|
||||
const openDeleteFolder = (folder: ChatFolder) => {
|
||||
const openDeleteFolder = (folder: StoredFolder) => {
|
||||
setDeletingFolder(folder);
|
||||
setIsDeleteFolderOpen(true);
|
||||
};
|
||||
@@ -149,37 +195,112 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat }: ChatSi
|
||||
setExpandedFolders(newExpanded);
|
||||
};
|
||||
|
||||
const moveToFolder = (conversationId: string, folderId: string) => {
|
||||
setConversations(
|
||||
conversations.map((conv) =>
|
||||
conv.id === conversationId ? { ...conv, folderId } : conv
|
||||
)
|
||||
);
|
||||
const moveToFolder = (chatId: string, folderId: string) => {
|
||||
try {
|
||||
const chat = chats.find(c => c.id === chatId);
|
||||
if (chat) {
|
||||
const updatedChat: StoredChat = {
|
||||
...chat,
|
||||
folderId: folderId,
|
||||
};
|
||||
chatService.saveChat(updatedChat);
|
||||
loadChatsAndFolders();
|
||||
|
||||
const folder = folders.find(f => f.id === folderId);
|
||||
toast({
|
||||
title: "Chat movido",
|
||||
description: `Movido para a pasta "${folder?.name}".`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erro ao mover chat",
|
||||
description: "Não foi possível mover o chat. Tente novamente.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeFromFolder = (conversationId: string) => {
|
||||
setConversations(
|
||||
conversations.map((conv) =>
|
||||
conv.id === conversationId ? { ...conv, folderId: undefined } : conv
|
||||
)
|
||||
);
|
||||
const removeFromFolder = (chatId: string) => {
|
||||
try {
|
||||
const chat = chats.find(c => c.id === chatId);
|
||||
if (chat) {
|
||||
const updatedChat: StoredChat = {
|
||||
...chat,
|
||||
folderId: undefined,
|
||||
};
|
||||
chatService.saveChat(updatedChat);
|
||||
loadChatsAndFolders();
|
||||
|
||||
toast({
|
||||
title: "Chat removido da pasta",
|
||||
description: "Chat movido para 'Sem Pasta'.",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erro ao remover chat",
|
||||
description: "Não foi possível remover o chat da pasta.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const filteredConversations = conversations.filter(
|
||||
(conv) =>
|
||||
conv.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
conv.lastMessage.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
const handleDeleteChat = () => {
|
||||
if (deletingChat) {
|
||||
try {
|
||||
chatService.deleteChat(deletingChat.id);
|
||||
loadChatsAndFolders();
|
||||
setDeletingChat(null);
|
||||
setIsDeleteChatOpen(false);
|
||||
|
||||
const conversationsWithoutFolder = filteredConversations.filter((c) => !c.folderId);
|
||||
const conversationsByFolder = folders.reduce((acc, folder) => {
|
||||
acc[folder.id] = filteredConversations.filter((c) => c.folderId === folder.id);
|
||||
toast({
|
||||
title: "Chat excluído",
|
||||
description: "A conversa foi excluída com sucesso.",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erro ao excluir chat",
|
||||
description: "Não foi possível excluir o chat. Tente novamente.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openDeleteChat = (chat: StoredChat) => {
|
||||
setDeletingChat(chat);
|
||||
setIsDeleteChatOpen(true);
|
||||
};
|
||||
|
||||
const handleSelectChat = (chat: StoredChat) => {
|
||||
if (onSelectChat) {
|
||||
onSelectChat(chat);
|
||||
}
|
||||
};
|
||||
|
||||
// Filtrar chats pela busca
|
||||
const filteredChats = chats.filter((chat) => {
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
const titleMatch = chat.title.toLowerCase().includes(searchLower);
|
||||
const contentMatch = chat.messages.some(msg =>
|
||||
msg.content.toLowerCase().includes(searchLower)
|
||||
);
|
||||
return titleMatch || contentMatch;
|
||||
});
|
||||
|
||||
// Separar chats sem pasta
|
||||
const chatsWithoutFolder = filteredChats.filter((c) => !c.folderId);
|
||||
|
||||
// Agrupar chats por pasta
|
||||
const chatsByFolder = folders.reduce((acc, folder) => {
|
||||
acc[folder.id] = filteredChats.filter((c) => c.folderId === folder.id);
|
||||
return acc;
|
||||
}, {} as Record<string, Conversation[]>);
|
||||
}, {} as Record<string, StoredChat[]>);
|
||||
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<div className="w-12 border-r border-border bg-card/30 backdrop-blur-sm flex flex-col items-center py-4">
|
||||
<div className="w-12 h-full border-r border-border bg-card/30 backdrop-blur-sm flex flex-col items-center py-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -193,7 +314,7 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat }: ChatSi
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-80 border-r border-border bg-card/30 backdrop-blur-sm flex flex-col">
|
||||
<div className="w-80 h-full border-r border-border bg-card/30 backdrop-blur-sm flex flex-col">
|
||||
{/* Header with collapse button */}
|
||||
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">Conversas</h3>
|
||||
@@ -330,10 +451,10 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat }: ChatSi
|
||||
{folder.name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{conversationsByFolder[folder.id]?.length || 0}
|
||||
{chatsByFolder[folder.id]?.length || 0}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -363,17 +484,18 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat }: ChatSi
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{expandedFolders.has(folder.id) && conversationsByFolder[folder.id]?.length > 0 && (
|
||||
{expandedFolders.has(folder.id) && chatsByFolder[folder.id]?.length > 0 && (
|
||||
<div className="ml-6 space-y-1">
|
||||
{conversationsByFolder[folder.id].map((conv) => (
|
||||
<ConversationItem
|
||||
key={conv.id}
|
||||
conversation={conv}
|
||||
isSelected={selectedConversation === conv.id}
|
||||
onSelect={() => setSelectedConversation(conv.id)}
|
||||
{chatsByFolder[folder.id].map((chat) => (
|
||||
<ChatItem
|
||||
key={chat.id}
|
||||
chat={chat}
|
||||
isSelected={currentChatId === chat.id}
|
||||
onSelect={() => handleSelectChat(chat)}
|
||||
folders={folders}
|
||||
onMoveToFolder={moveToFolder}
|
||||
onRemoveFromFolder={removeFromFolder}
|
||||
onDelete={() => openDeleteChat(chat)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -381,48 +503,90 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat }: ChatSi
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Conversations without folder */}
|
||||
{conversationsWithoutFolder.length > 0 && (
|
||||
{/* Chats without folder */}
|
||||
{chatsWithoutFolder.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Sem Pasta
|
||||
</div>
|
||||
{conversationsWithoutFolder.map((conv) => (
|
||||
<ConversationItem
|
||||
key={conv.id}
|
||||
conversation={conv}
|
||||
isSelected={selectedConversation === conv.id}
|
||||
onSelect={() => setSelectedConversation(conv.id)}
|
||||
{chatsWithoutFolder.map((chat) => (
|
||||
<ChatItem
|
||||
key={chat.id}
|
||||
chat={chat}
|
||||
isSelected={currentChatId === chat.id}
|
||||
onSelect={() => handleSelectChat(chat)}
|
||||
folders={folders}
|
||||
onMoveToFolder={moveToFolder}
|
||||
onRemoveFromFolder={removeFromFolder}
|
||||
onDelete={() => openDeleteChat(chat)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Delete Chat Alert */}
|
||||
<AlertDialog open={isDeleteChatOpen} onOpenChange={setIsDeleteChatOpen}>
|
||||
<AlertDialogContent className="glass-effect bg-card border-border z-50">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Excluir Conversa?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Esta ação não pode ser desfeita. A conversa "{deletingChat?.title}" será permanentemente excluída.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteChat} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
Excluir
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ConversationItemProps {
|
||||
conversation: Conversation;
|
||||
interface ChatItemProps {
|
||||
chat: StoredChat;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
folders: ChatFolder[];
|
||||
onMoveToFolder: (conversationId: string, folderId: string) => void;
|
||||
onRemoveFromFolder: (conversationId: string) => void;
|
||||
folders: StoredFolder[];
|
||||
onMoveToFolder: (chatId: string, folderId: string) => void;
|
||||
onRemoveFromFolder: (chatId: string) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const ConversationItem = ({
|
||||
conversation,
|
||||
const ChatItem = ({
|
||||
chat,
|
||||
isSelected,
|
||||
onSelect,
|
||||
folders,
|
||||
onMoveToFolder,
|
||||
onRemoveFromFolder,
|
||||
}: ConversationItemProps) => {
|
||||
onDelete,
|
||||
}: ChatItemProps) => {
|
||||
// Pega a última mensagem do usuário
|
||||
const lastUserMessage = chat.messages
|
||||
.filter(m => m.role === 'user')
|
||||
.slice(-1)[0];
|
||||
|
||||
// Formata timestamp relativo
|
||||
const timeAgo = formatDistanceToNow(new Date(chat.updatedAt), {
|
||||
addSuffix: true,
|
||||
locale: ptBR,
|
||||
});
|
||||
|
||||
// Limita o título a 40 caracteres
|
||||
const truncatedTitle = chat.title.length > 40
|
||||
? chat.title.substring(0, 40) + '...'
|
||||
: chat.title;
|
||||
|
||||
// Limita a mensagem a 50 caracteres
|
||||
const truncatedMessage = lastUserMessage?.content.length > 50
|
||||
? lastUserMessage.content.substring(0, 50) + '...'
|
||||
: lastUserMessage?.content;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group flex items-start gap-2 px-3 py-2 rounded-lg cursor-pointer transition-all ${
|
||||
@@ -433,13 +597,17 @@ const ConversationItem = ({
|
||||
onClick={onSelect}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 mt-0.5 text-primary flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{conversation.title}</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{conversation.lastMessage}
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<p className="text-sm font-medium truncate" title={chat.title}>
|
||||
{truncatedTitle}
|
||||
</p>
|
||||
{lastUserMessage && (
|
||||
<p className="text-xs text-muted-foreground truncate" title={lastUserMessage.content}>
|
||||
{truncatedMessage}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{conversation.timestamp}
|
||||
{timeAgo}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -448,21 +616,16 @@ const ConversationItem = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
||||
>
|
||||
<MoreVertical className="w-3 h-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="glass-effect bg-popover border-border z-50">
|
||||
<DropdownMenuItem className="gap-2">
|
||||
<Edit className="w-3 h-3" />
|
||||
Renomear
|
||||
</DropdownMenuItem>
|
||||
|
||||
{conversation.folderId && (
|
||||
{chat.folderId && (
|
||||
<DropdownMenuItem
|
||||
className="gap-2"
|
||||
onClick={() => onRemoveFromFolder(conversation.id)}
|
||||
onClick={() => onRemoveFromFolder(chat.id)}
|
||||
>
|
||||
<FolderInput className="w-3 h-3" />
|
||||
Remover da Pasta
|
||||
@@ -473,14 +636,17 @@ const ConversationItem = ({
|
||||
<DropdownMenuItem
|
||||
key={folder.id}
|
||||
className="gap-2"
|
||||
onClick={() => onMoveToFolder(conversation.id, folder.id)}
|
||||
onClick={() => onMoveToFolder(chat.id, folder.id)}
|
||||
>
|
||||
<Folder className="w-3 h-3" />
|
||||
Mover para {folder.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
|
||||
<DropdownMenuItem className="gap-2 text-destructive">
|
||||
<DropdownMenuItem
|
||||
className="gap-2 text-destructive"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Excluir
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ChatHeader } from "@/components/ChatHeader";
|
||||
import { ChatMessage } from "./ChatMessage";
|
||||
import { ChatInput } from "./ChatInput";
|
||||
import { ChatSidebar } from "./ChatSidebar";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { chatService, StoredChat } from "@/services/chat";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { getModelId } from "@/config/models";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
@@ -18,24 +21,61 @@ interface Message {
|
||||
}
|
||||
|
||||
export const ChatView = () => {
|
||||
const { toast } = useToast();
|
||||
const [isChatSidebarCollapsed, setIsChatSidebarCollapsed] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState("ChatGPT 4.1");
|
||||
const [currentChatId, setCurrentChatId] = useState("1");
|
||||
const [selectedModel, setSelectedModel] = useState("GPT-4o");
|
||||
// Inicia com "0" - será atualizado com o chat_id real após primeira resposta da API
|
||||
const [currentChatId, setCurrentChatId] = useState("0");
|
||||
const [systemPrompt, setSystemPrompt] = useState("Você é um assistente útil e prestativo.");
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: "1",
|
||||
role: "assistant",
|
||||
content: "Olá! Sou o assistente HGTX Codex. Como posso ajudá-lo hoje?",
|
||||
model: "ChatGPT 4.1",
|
||||
model: "GPT-4o",
|
||||
},
|
||||
]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Salva o chat no localStorage sempre que as mensagens mudam
|
||||
useEffect(() => {
|
||||
if (messages.length > 1) { // Salva apenas se houver mensagens além da inicial
|
||||
saveCurrentChat();
|
||||
// Dispara evento customizado para a sidebar recarregar
|
||||
window.dispatchEvent(new Event('chatUpdated'));
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
// Função para salvar o chat atual
|
||||
const saveCurrentChat = () => {
|
||||
try {
|
||||
const chatTitle = chatService.generateChatTitle(
|
||||
messages.find(m => m.role === 'user')?.content || 'Nova Conversa'
|
||||
);
|
||||
|
||||
const storedChat: StoredChat = {
|
||||
id: currentChatId,
|
||||
title: chatTitle,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
model: selectedModel,
|
||||
systemPrompt: systemPrompt,
|
||||
messages: messages.map(msg => ({
|
||||
...msg,
|
||||
timestamp: new Date(),
|
||||
})),
|
||||
};
|
||||
|
||||
chatService.saveChat(storedChat);
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar chat:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewChat = () => {
|
||||
const newChatId = Date.now().toString();
|
||||
setCurrentChatId(newChatId);
|
||||
// Reseta para "0" - novo chat sempre começa com chat_id "0"
|
||||
setCurrentChatId("0");
|
||||
setMessages([
|
||||
{
|
||||
id: "1",
|
||||
@@ -44,9 +84,31 @@ export const ChatView = () => {
|
||||
model: selectedModel,
|
||||
},
|
||||
]);
|
||||
|
||||
// Dispara evento para atualizar a sidebar
|
||||
window.dispatchEvent(new Event('chatUpdated'));
|
||||
};
|
||||
|
||||
const handleSendMessage = (content: string, files?: File[]) => {
|
||||
const handleLoadChat = (chat: StoredChat) => {
|
||||
// Carrega um chat existente do histórico
|
||||
setCurrentChatId(chat.id);
|
||||
setSelectedModel(chat.model);
|
||||
setSystemPrompt(chat.systemPrompt);
|
||||
|
||||
// Converte mensagens do StoredChat para Message
|
||||
const loadedMessages: Message[] = chat.messages.map(msg => ({
|
||||
id: msg.id,
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
model: msg.model,
|
||||
attachments: msg.attachments,
|
||||
}));
|
||||
|
||||
setMessages(loadedMessages);
|
||||
};
|
||||
|
||||
const handleSendMessage = async (content: string, files?: File[]) => {
|
||||
// Adiciona mensagem do usuário na interface
|
||||
const newMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: "user",
|
||||
@@ -61,28 +123,71 @@ export const ChatView = () => {
|
||||
setMessages((prev) => [...prev, newMessage]);
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate AI response
|
||||
setTimeout(() => {
|
||||
const aiResponse: Message = {
|
||||
try {
|
||||
// Envia mensagem para a API
|
||||
// Converte o nome do modelo para o ID da API
|
||||
const modelId = getModelId(selectedModel);
|
||||
|
||||
const response = await chatService.sendMessage({
|
||||
chatId: currentChatId,
|
||||
message: content,
|
||||
modelId: modelId,
|
||||
personalidade: systemPrompt,
|
||||
anexos: files,
|
||||
});
|
||||
|
||||
// Adiciona resposta da IA na interface
|
||||
if (response.success) {
|
||||
// IMPORTANTE: Atualiza o chat_id com o valor retornado pela API
|
||||
// Isso mantém o contexto da conversa para as próximas mensagens
|
||||
if (response.chat_id && response.chat_id !== currentChatId) {
|
||||
console.log(`Chat ID atualizado: ${currentChatId} → ${response.chat_id}`);
|
||||
setCurrentChatId(response.chat_id);
|
||||
}
|
||||
|
||||
const aiResponse: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: "assistant",
|
||||
content: response.response,
|
||||
model: selectedModel,
|
||||
};
|
||||
setMessages((prev) => [...prev, aiResponse]);
|
||||
} else {
|
||||
throw new Error('Resposta inválida da API');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Erro ao enviar mensagem:', error);
|
||||
|
||||
// Mostra toast de erro para o usuário
|
||||
toast({
|
||||
title: "Erro ao enviar mensagem",
|
||||
description: error.message || "Não foi possível comunicar com a IA. Tente novamente.",
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
// Adiciona mensagem de erro na interface
|
||||
const errorMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: "assistant",
|
||||
content:
|
||||
`Resposta com base na personalidade: "${systemPrompt}". Esta é uma resposta simulada que considera as instruções de sistema definidas.`,
|
||||
content: "Desculpe, ocorreu um erro ao processar sua mensagem. Por favor, tente novamente.",
|
||||
model: selectedModel,
|
||||
};
|
||||
setMessages((prev) => [...prev, aiResponse]);
|
||||
setMessages((prev) => [...prev, errorMessage]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full pb-16 md:pb-0">
|
||||
{/* Chat Sidebar - Hidden on mobile */}
|
||||
<div className="hidden md:block">
|
||||
<ChatSidebar
|
||||
<ChatSidebar
|
||||
isCollapsed={isChatSidebarCollapsed}
|
||||
onToggleCollapse={() => setIsChatSidebarCollapsed(!isChatSidebarCollapsed)}
|
||||
onNewChat={handleNewChat}
|
||||
onSelectChat={handleLoadChat}
|
||||
currentChatId={currentChatId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ChatHeader } from "@/components/ChatHeader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -13,56 +13,133 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { imageGenerationService, IMAGE_SIZE_OPTIONS, ImageSize } from "@/services/imageGeneration";
|
||||
|
||||
interface GeneratedImage {
|
||||
id: string;
|
||||
url: string;
|
||||
prompt: string;
|
||||
size: string;
|
||||
timestamp: string;
|
||||
size: ImageSize;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export const ImageView = () => {
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [selectedSize, setSelectedSize] = useState("1024x1024");
|
||||
const [selectedSize, setSelectedSize] = useState<ImageSize>("1024x1024");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [images, setImages] = useState<GeneratedImage[]>([
|
||||
{
|
||||
id: "1",
|
||||
url: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=512&h=512&fit=crop",
|
||||
prompt: "Paisagem futurista com cidades voadoras",
|
||||
size: "1024x1024",
|
||||
timestamp: "Há 2 horas",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
url: "https://images.unsplash.com/photo-1634017839464-5c339ebe3cb4?w=512&h=512&fit=crop",
|
||||
prompt: "Robô humanoide em estilo cyberpunk",
|
||||
size: "1024x1536",
|
||||
timestamp: "Há 5 horas",
|
||||
},
|
||||
]);
|
||||
const [images, setImages] = useState<GeneratedImage[]>([]);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Carrega histórico do localStorage ao montar o componente
|
||||
useEffect(() => {
|
||||
const savedImages = localStorage.getItem('imageHistory');
|
||||
if (savedImages) {
|
||||
try {
|
||||
const parsedImages = JSON.parse(savedImages);
|
||||
// Converte strings de data de volta para Date objects
|
||||
const imagesWithDates = parsedImages.map((img: any) => ({
|
||||
...img,
|
||||
timestamp: new Date(img.timestamp),
|
||||
}));
|
||||
setImages(imagesWithDates);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar histórico de imagens:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
// Valida a descrição antes de enviar
|
||||
const validation = imageGenerationService.validateDescription(prompt);
|
||||
if (!validation.valid) {
|
||||
toast({
|
||||
title: "Descrição inválida",
|
||||
description: validation.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const handleGenerate = () => {
|
||||
setIsGenerating(true);
|
||||
// Simulate image generation
|
||||
setTimeout(() => {
|
||||
const newImage: GeneratedImage = {
|
||||
id: Date.now().toString(),
|
||||
url: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=512&h=512&fit=crop",
|
||||
prompt: prompt,
|
||||
|
||||
try {
|
||||
// Chama o serviço de geração de imagem
|
||||
const response = await imageGenerationService.generateImage({
|
||||
description: prompt,
|
||||
size: selectedSize,
|
||||
timestamp: "Agora",
|
||||
};
|
||||
setImages([newImage, ...images]);
|
||||
});
|
||||
|
||||
// Verifica se a geração foi bem-sucedida
|
||||
if (response.success) {
|
||||
// Log da URL da imagem para debug
|
||||
console.log('URL da imagem gerada:', response.image_url);
|
||||
|
||||
const newImage: GeneratedImage = {
|
||||
id: response.image_generation_id,
|
||||
url: response.image_url,
|
||||
prompt: response.message,
|
||||
size: selectedSize,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
const newHistory = [newImage, ...images].slice(0, 20); // Mantém apenas as últimas 20 imagens
|
||||
setImages(newHistory);
|
||||
localStorage.setItem('imageHistory', JSON.stringify(newHistory));
|
||||
|
||||
toast({
|
||||
title: "Imagem gerada com sucesso",
|
||||
description: `Tamanho: ${IMAGE_SIZE_OPTIONS[selectedSize].label}`,
|
||||
});
|
||||
|
||||
// Limpa o campo de descrição após sucesso
|
||||
setPrompt("");
|
||||
} else {
|
||||
throw new Error('Erro ao gerar imagem');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Erro na geração de imagem:', error);
|
||||
|
||||
toast({
|
||||
title: "Erro na geração",
|
||||
description: error.message || "Não foi possível gerar a imagem. Tente novamente.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
setPrompt("");
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setImages(images.filter((img) => img.id !== id));
|
||||
const newHistory = images.filter((img) => img.id !== id);
|
||||
setImages(newHistory);
|
||||
localStorage.setItem('imageHistory', JSON.stringify(newHistory));
|
||||
|
||||
toast({
|
||||
title: "Imagem removida",
|
||||
description: "A imagem foi removida do histórico.",
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = async (image: GeneratedImage) => {
|
||||
try {
|
||||
await imageGenerationService.downloadImage(
|
||||
image.url,
|
||||
`${image.prompt.substring(0, 30)}_${image.size}.png`
|
||||
);
|
||||
|
||||
toast({
|
||||
title: "Download iniciado",
|
||||
description: "A imagem está sendo baixada.",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erro no download",
|
||||
description: "Não foi possível baixar a imagem.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const filteredImages = images.filter((img) =>
|
||||
@@ -107,14 +184,14 @@ export const ImageView = () => {
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
Tamanho
|
||||
</label>
|
||||
<Select value={selectedSize} onValueChange={setSelectedSize}>
|
||||
<Select value={selectedSize} onValueChange={(value) => setSelectedSize(value as ImageSize)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="glass-effect bg-popover border-border z-50">
|
||||
<SelectItem value="1024x1024">1024x1024 (Quadrado)</SelectItem>
|
||||
<SelectItem value="1024x1536">1024x1536 (Retrato)</SelectItem>
|
||||
<SelectItem value="1536x1024">1536x1024 (Paisagem)</SelectItem>
|
||||
<SelectItem value="1024x1792">1024x1792 (Retrato)</SelectItem>
|
||||
<SelectItem value="1792x1024">1792x1024 (Paisagem)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -153,9 +230,10 @@ export const ImageView = () => {
|
||||
{!isGenerating && images.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Última Geração</h3>
|
||||
<ImageCard
|
||||
image={images[0]}
|
||||
<ImageCard
|
||||
image={images[0]}
|
||||
onDelete={handleDelete}
|
||||
onDownload={handleDownload}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -178,10 +256,11 @@ export const ImageView = () => {
|
||||
{filteredImages.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredImages.map((image) => (
|
||||
<ImageCard
|
||||
key={image.id}
|
||||
<ImageCard
|
||||
key={image.id}
|
||||
image={image}
|
||||
onDelete={handleDelete}
|
||||
onDownload={handleDownload}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -205,29 +284,93 @@ export const ImageView = () => {
|
||||
interface ImageCardProps {
|
||||
image: GeneratedImage;
|
||||
onDelete: (id: string) => void;
|
||||
onDownload: (image: GeneratedImage) => void;
|
||||
}
|
||||
|
||||
const ImageCard = ({ image, onDelete }: ImageCardProps) => {
|
||||
const ImageCard = ({ image, onDelete, onDownload }: ImageCardProps) => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [imageLoading, setImageLoading] = useState(true);
|
||||
|
||||
// Calcula tempo relativo
|
||||
const getRelativeTime = (timestamp: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - new Date(timestamp).getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return 'Agora';
|
||||
if (minutes < 60) return `Há ${minutes} ${minutes === 1 ? 'minuto' : 'minutos'}`;
|
||||
if (hours < 24) return `Há ${hours} ${hours === 1 ? 'hora' : 'horas'}`;
|
||||
return `Há ${days} ${days === 1 ? 'dia' : 'dias'}`;
|
||||
};
|
||||
|
||||
const handleImageError = () => {
|
||||
console.warn('Erro CORS ao carregar imagem:', image.url);
|
||||
setImageError(true);
|
||||
setImageLoading(false);
|
||||
};
|
||||
|
||||
const handleImageLoad = () => {
|
||||
console.log('Imagem carregada com sucesso:', image.url);
|
||||
setImageLoading(false);
|
||||
setImageError(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group glass-effect rounded-xl overflow-hidden animate-fade-in">
|
||||
<div className="relative aspect-square">
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.prompt}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{imageLoading && !imageError && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-muted">
|
||||
<div className="w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
{imageError ? (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-muted text-muted-foreground p-4">
|
||||
<Sparkles className="w-12 h-12 mb-2 opacity-50" />
|
||||
<p className="text-sm text-center font-medium mb-1">Erro ao carregar</p>
|
||||
<p className="text-xs text-center mb-2">A imagem foi gerada, mas não pode ser exibida aqui</p>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<a
|
||||
href={image.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-center bg-primary text-primary-foreground px-3 py-2 rounded-md hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Abrir imagem em nova aba
|
||||
</a>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(image.url)}
|
||||
className="text-xs text-center bg-secondary text-secondary-foreground px-3 py-1 rounded-md hover:bg-secondary/80 transition-colors"
|
||||
>
|
||||
Copiar URL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.prompt}
|
||||
className="w-full h-full object-cover"
|
||||
onError={handleImageError}
|
||||
onLoad={handleImageLoad}
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/0 to-black/0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 space-y-2">
|
||||
<p className="text-sm text-white line-clamp-2">{image.prompt}</p>
|
||||
<div className="flex items-center justify-between text-xs text-white/70">
|
||||
<span>{image.size}</span>
|
||||
<span>{image.timestamp}</span>
|
||||
<span>{IMAGE_SIZE_OPTIONS[image.size].label}</span>
|
||||
<span>{getRelativeTime(image.timestamp)}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="flex-1 gap-1"
|
||||
onClick={() => onDownload(image)}
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
Baixar
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Configuração de modelos de IA disponíveis
|
||||
* Mapeia os nomes de exibição para os IDs usados pela API
|
||||
*/
|
||||
|
||||
export interface ModelConfig {
|
||||
name: string; // Nome exibido na interface
|
||||
id: string; // ID enviado para a API
|
||||
description?: string; // Descrição opcional do modelo
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista de modelos disponíveis
|
||||
* NOTA: Estes dados correspondem à tabela de modelos do banco de dados.
|
||||
* Futuramente será integrado com API para buscar dinamicamente.
|
||||
*/
|
||||
export const AVAILABLE_MODELS: ModelConfig[] = [
|
||||
{
|
||||
name: 'GPT-4.1',
|
||||
id: '1',
|
||||
description: 'Modelo GPT-4.1',
|
||||
},
|
||||
{
|
||||
name: 'GPT-4o',
|
||||
id: '2',
|
||||
description: 'Modelo GPT-4o',
|
||||
},
|
||||
{
|
||||
name: 'GPT-5 Mini',
|
||||
id: '3',
|
||||
description: 'Modelo GPT-5 Mini',
|
||||
},
|
||||
{
|
||||
name: 'Gemini 2.0 Flash',
|
||||
id: '4',
|
||||
description: 'Google Gemini 2.0 Flash',
|
||||
},
|
||||
{
|
||||
name: 'Claude Sonnet 4.5',
|
||||
id: '5',
|
||||
description: 'Anthropic Claude Sonnet 4.5',
|
||||
},
|
||||
{
|
||||
name: 'DeepSeek V3.2 Chat',
|
||||
id: '6',
|
||||
description: 'DeepSeek V3.2 Chat',
|
||||
},
|
||||
{
|
||||
name: 'DeepSeek V3.2 Reasoner',
|
||||
id: '7',
|
||||
description: 'DeepSeek V3.2 Reasoner',
|
||||
},
|
||||
{
|
||||
name: 'Gemini 2.5 Flash',
|
||||
id: '8',
|
||||
description: 'Google Gemini 2.5 Flash',
|
||||
},
|
||||
{
|
||||
name: 'Gemini 2.5 Flash-Lite',
|
||||
id: '9',
|
||||
description: 'Google Gemini 2.5 Flash-Lite',
|
||||
},
|
||||
{
|
||||
name: 'Claude Haiku 4.5',
|
||||
id: '10',
|
||||
description: 'Anthropic Claude Haiku 4.5',
|
||||
},
|
||||
{
|
||||
name: 'GPT-4o Mini',
|
||||
id: '11',
|
||||
description: 'Modelo GPT-4o Mini',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapeia o nome do modelo para o ID da API
|
||||
*
|
||||
* @param modelName - Nome do modelo exibido na interface
|
||||
* @returns ID do modelo para a API, ou o próprio nome se não encontrado
|
||||
*/
|
||||
export function getModelId(modelName: string): string {
|
||||
const model = AVAILABLE_MODELS.find(m => m.name === modelName);
|
||||
return model?.id || modelName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapeia o ID da API para o nome de exibição
|
||||
*
|
||||
* @param modelId - ID do modelo na API
|
||||
* @returns Nome do modelo para exibir, ou o próprio ID se não encontrado
|
||||
*/
|
||||
export function getModelName(modelId: string): string {
|
||||
const model = AVAILABLE_MODELS.find(m => m.id === modelId);
|
||||
return model?.name || modelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém a configuração completa de um modelo pelo nome
|
||||
*
|
||||
* @param modelName - Nome do modelo
|
||||
* @returns Configuração do modelo ou undefined
|
||||
*/
|
||||
export function getModelConfig(modelName: string): ModelConfig | undefined {
|
||||
return AVAILABLE_MODELS.find(m => m.name === modelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna apenas os nomes dos modelos para uso em dropdowns
|
||||
*/
|
||||
export function getModelNames(): string[] {
|
||||
return AVAILABLE_MODELS.map(m => m.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* FUTURA INTEGRAÇÃO COM BANCO DE DADOS
|
||||
*
|
||||
* Quando a API estiver pronta, substituir AVAILABLE_MODELS por chamada à API:
|
||||
*
|
||||
* export async function fetchModelsFromDatabase(): Promise<ModelConfig[]> {
|
||||
* const response = await apiService.get('/api/models');
|
||||
* return response.data.map((model: any) => ({
|
||||
* name: model.name,
|
||||
* id: model.id.toString(),
|
||||
* description: model.description || '',
|
||||
* }));
|
||||
* }
|
||||
*
|
||||
* Nos componentes, usar:
|
||||
* - useEffect para carregar modelos na montagem
|
||||
* - useState para armazenar lista de modelos
|
||||
* - Loading state durante o fetch
|
||||
*/
|
||||
@@ -0,0 +1,127 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
/**
|
||||
* Configuração centralizada da API
|
||||
* Todas as chamadas de API devem usar este serviço para garantir
|
||||
* autenticação e configuração consistente
|
||||
*/
|
||||
class ApiService {
|
||||
private axiosInstance: AxiosInstance;
|
||||
private apiKey: string;
|
||||
private baseURL: string;
|
||||
|
||||
constructor() {
|
||||
// Busca configurações das variáveis de ambiente
|
||||
this.apiKey = import.meta.env.VITE_API_KEY || '';
|
||||
this.baseURL = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
// Validação das variáveis de ambiente
|
||||
if (!this.apiKey) {
|
||||
console.error('VITE_API_KEY não configurada no arquivo .env');
|
||||
}
|
||||
if (!this.baseURL) {
|
||||
console.error('VITE_API_BASE_URL não configurada no arquivo .env');
|
||||
}
|
||||
|
||||
// Cria instância do Axios com configurações padrão
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: this.baseURL,
|
||||
timeout: 60000, // 60 segundos para upload de arquivos
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Interceptor para adicionar API Key em todas as requisições
|
||||
this.axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
// Adiciona a API Key no header de todas as requisições
|
||||
if (this.apiKey) {
|
||||
config.headers['apikey'] = this.apiKey;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Interceptor de resposta para tratamento centralizado de erros
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// Log de erro para debug
|
||||
console.error('API Error:', {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
data: error.response?.data,
|
||||
});
|
||||
|
||||
// Retorna erro formatado
|
||||
return Promise.reject({
|
||||
message: error.response?.data?.message || error.message || 'Erro ao comunicar com o servidor',
|
||||
status: error.response?.status,
|
||||
data: error.response?.data,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requisição GET
|
||||
*/
|
||||
async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.axiosInstance.get<T>(url, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requisição POST
|
||||
*/
|
||||
async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.axiosInstance.post<T>(url, data, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requisição POST com FormData (para upload de arquivos)
|
||||
*/
|
||||
async postFormData<T = any>(url: string, formData: FormData, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.axiosInstance.post<T>(url, formData, {
|
||||
...config,
|
||||
headers: {
|
||||
...config?.headers,
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Requisição PUT
|
||||
*/
|
||||
async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.axiosInstance.put<T>(url, data, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requisição DELETE
|
||||
*/
|
||||
async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.axiosInstance.delete<T>(url, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna a URL base configurada
|
||||
*/
|
||||
getBaseURL(): string {
|
||||
return this.baseURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna a instância do Axios (uso avançado)
|
||||
*/
|
||||
getInstance(): AxiosInstance {
|
||||
return this.axiosInstance;
|
||||
}
|
||||
}
|
||||
|
||||
// Exporta instância única (Singleton)
|
||||
export const apiService = new ApiService();
|
||||
@@ -0,0 +1,197 @@
|
||||
import { apiService } from './api';
|
||||
|
||||
/**
|
||||
* Tipos de vozes disponíveis para geração de áudio
|
||||
*/
|
||||
export type VoiceType = 'alloy' | 'echo' | 'fable' | 'nova' | 'onyx' | 'shimmer';
|
||||
|
||||
/**
|
||||
* Interface para a resposta da API de geração de áudio
|
||||
*/
|
||||
export interface AudioGenerationResponse {
|
||||
success: boolean;
|
||||
audio_url: string; // URL do áudio gerado
|
||||
audio_generation_id: string;
|
||||
message: string; // Texto que foi convertido em áudio
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface para os dados necessários para geração de áudio
|
||||
*/
|
||||
export interface AudioGenerationRequest {
|
||||
message: string;
|
||||
voice: VoiceType;
|
||||
userEmail?: string;
|
||||
estabelecimentoId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Informações sobre cada tipo de voz disponível
|
||||
*/
|
||||
export const VOICE_OPTIONS = {
|
||||
alloy: {
|
||||
label: "Alloy",
|
||||
gender: "Masculina",
|
||||
style: "Neutra, equilibrada, tom corporativo",
|
||||
description: "Boa para tutoriais e comunicações institucionais."
|
||||
},
|
||||
echo: {
|
||||
label: "Echo",
|
||||
gender: "Masculina",
|
||||
style: "Forte e profissional, mais grave",
|
||||
description: "Ideal para voz de autoridade ou locução firme."
|
||||
},
|
||||
fable: {
|
||||
label: "Fable",
|
||||
gender: "Feminina",
|
||||
style: "Narrativa, calorosa e envolvente",
|
||||
description: "Ótima para storytelling e áudios empáticos."
|
||||
},
|
||||
onyx: {
|
||||
label: "Onyx",
|
||||
gender: "Masculina",
|
||||
style: "Grave, autoritária, impactante",
|
||||
description: "Excelente para trailers, mensagens sérias ou institucionais."
|
||||
},
|
||||
nova: {
|
||||
label: "Nova",
|
||||
gender: "Feminina",
|
||||
style: "Brilhante, animada, energética",
|
||||
description: "Boa para vídeos curtos, marketing ou conteúdos leves."
|
||||
},
|
||||
shimmer: {
|
||||
label: "Shimmer",
|
||||
gender: "Feminina",
|
||||
style: "Suave, otimista, clara",
|
||||
description: "Boa para mensagens acolhedoras, explicações e IA conversacional."
|
||||
}
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Serviço de geração de áudio (Text-to-Speech)
|
||||
*/
|
||||
class AudioGenerationService {
|
||||
private readonly AUDIO_GENERATION_ENDPOINT = '/webhook/codex/gerar_audio';
|
||||
|
||||
/**
|
||||
* Gera um arquivo de áudio a partir de texto
|
||||
*
|
||||
* @param request - Dados da requisição (texto, voz, email, estabelecimento)
|
||||
* @returns Promise com a resposta da API
|
||||
*/
|
||||
async generateAudio(request: AudioGenerationRequest): Promise<AudioGenerationResponse> {
|
||||
const { message, voice, userEmail, estabelecimentoId } = request;
|
||||
|
||||
// Usa valores do .env se não forem fornecidos
|
||||
const email = userEmail || import.meta.env.VITE_USER_EMAIL || '';
|
||||
const estabId = estabelecimentoId || parseInt(import.meta.env.VITE_ESTABELECIMENTO_ID) || 1;
|
||||
|
||||
// Valida o texto
|
||||
if (!message || message.trim().length === 0) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'O texto não pode estar vazio',
|
||||
};
|
||||
}
|
||||
|
||||
// Valida a voz
|
||||
if (!this.isValidVoice(voice)) {
|
||||
throw {
|
||||
success: false,
|
||||
message: `Voz inválida. Opções disponíveis: ${Object.keys(VOICE_OPTIONS).join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Log para debug (remover em produção se necessário)
|
||||
console.log('Gerando áudio:', {
|
||||
messageLength: message.length,
|
||||
voice,
|
||||
userEmail: email,
|
||||
estabelecimentoId: estabId,
|
||||
});
|
||||
|
||||
try {
|
||||
// Faz a requisição usando o serviço de API
|
||||
const response = await apiService.post<AudioGenerationResponse>(
|
||||
this.AUDIO_GENERATION_ENDPOINT,
|
||||
{
|
||||
estabelecimento_id: estabId,
|
||||
user_email: email,
|
||||
message: message,
|
||||
voice: voice,
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
// Trata erros específicos
|
||||
console.error('Erro na geração de áudio:', error);
|
||||
|
||||
throw {
|
||||
success: false,
|
||||
message: error.message || 'Erro ao gerar áudio',
|
||||
status: error.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se a voz selecionada é suportada
|
||||
*
|
||||
* @param voice - Voz a ser validada
|
||||
* @returns true se a voz é válida
|
||||
*/
|
||||
isValidVoice(voice: string): voice is VoiceType {
|
||||
return Object.keys(VOICE_OPTIONS).includes(voice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém informações sobre uma voz específica
|
||||
*
|
||||
* @param voice - Tipo de voz
|
||||
* @returns Informações da voz
|
||||
*/
|
||||
getVoiceInfo(voice: VoiceType) {
|
||||
return VOICE_OPTIONS[voice];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista todas as vozes disponíveis
|
||||
*
|
||||
* @returns Array com todas as opções de voz
|
||||
*/
|
||||
getAllVoices() {
|
||||
return Object.entries(VOICE_OPTIONS).map(([key, info]) => ({
|
||||
value: key as VoiceType,
|
||||
...info,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida o texto para geração de áudio
|
||||
*
|
||||
* @param text - Texto a ser validado
|
||||
* @param maxLength - Comprimento máximo (padrão: 4096 caracteres)
|
||||
* @returns Objeto com resultado da validação
|
||||
*/
|
||||
validateText(text: string, maxLength: number = 4096): { valid: boolean; error?: string } {
|
||||
if (!text || text.trim().length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'O texto não pode estar vazio',
|
||||
};
|
||||
}
|
||||
|
||||
if (text.length > maxLength) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `O texto é muito longo. Máximo: ${maxLength} caracteres`,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Exporta instância única (Singleton)
|
||||
export const audioGenerationService = new AudioGenerationService();
|
||||
@@ -0,0 +1,481 @@
|
||||
import { apiService } from './api';
|
||||
|
||||
/**
|
||||
* Interface para a resposta da API de chat
|
||||
*/
|
||||
export interface ChatResponse {
|
||||
success: boolean;
|
||||
response: string; // Mensagem da IA
|
||||
chat_id: string; // ID do chat retornado pela API (importante para manter contexto)
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface para os dados necessários para enviar mensagem
|
||||
*/
|
||||
export interface ChatMessageRequest {
|
||||
chatId: string;
|
||||
message: string;
|
||||
modelId: string;
|
||||
personalidade?: string;
|
||||
anexos?: File[]; // Array de até 5 arquivos
|
||||
userEmail?: string;
|
||||
estabelecimentoId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface para armazenamento local de conversas
|
||||
* Preparando para futura integração com banco de dados
|
||||
*/
|
||||
export interface StoredChat {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
model: string;
|
||||
systemPrompt: string;
|
||||
messages: Array<{
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
model?: string;
|
||||
timestamp: Date;
|
||||
attachments?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface para armazenamento de pastas
|
||||
* Preparando para futura integração com banco de dados
|
||||
*/
|
||||
export interface StoredFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
chatIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Serviço de chat com IA
|
||||
*/
|
||||
class ChatService {
|
||||
private readonly CHAT_ENDPOINT = '/webhook/codex/message';
|
||||
private readonly STORAGE_KEY_CHATS = 'hgtx_chats';
|
||||
private readonly STORAGE_KEY_FOLDERS = 'hgtx_folders';
|
||||
|
||||
// Formatos de arquivo permitidos (atualmente)
|
||||
private readonly ALLOWED_FILE_TYPES = {
|
||||
// Formatos ativos
|
||||
'application/pdf': { ext: '.pdf', label: 'PDF' },
|
||||
'image/png': { ext: '.png', label: 'PNG' },
|
||||
'image/jpeg': { ext: '.jpg, .jpeg', label: 'JPEG' },
|
||||
'image/webp': { ext: '.webp', label: 'WebP' },
|
||||
|
||||
// Formatos futuros (desabilitados por enquanto)
|
||||
// 'text/csv': { ext: '.csv', label: 'CSV' },
|
||||
// 'text/plain': { ext: '.txt', label: 'TXT' },
|
||||
// 'application/json': { ext: '.json', label: 'JSON' },
|
||||
// 'application/vnd.ms-excel': { ext: '.xls', label: 'XLS' },
|
||||
// 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { ext: '.xlsx', label: 'XLSX' },
|
||||
};
|
||||
|
||||
// Número máximo de anexos permitidos
|
||||
private readonly MAX_ATTACHMENTS = 5;
|
||||
|
||||
/**
|
||||
* Gera um chat_id único e seguro usando hash
|
||||
* Formato: timestamp + random + hash
|
||||
*
|
||||
* NOTA: Esta função está mantida para compatibilidade e uso no localStorage,
|
||||
* mas para comunicação com a API, o fluxo correto é:
|
||||
* 1. Enviar chat_id: "0" na primeira mensagem
|
||||
* 2. API retorna o chat_id real
|
||||
* 3. Usar o chat_id retornado nas próximas mensagens
|
||||
*
|
||||
* @returns string - ID único para o chat (uso local)
|
||||
*/
|
||||
generateChatId(): string {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const randomPart = Math.random().toString(36).substring(2, 15);
|
||||
const randomPart2 = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
// Combina timestamp e partes aleatórias para criar ID único
|
||||
const chatId = `chat_${timestamp}_${randomPart}${randomPart2}`;
|
||||
|
||||
return chatId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envia mensagem para a API de chat
|
||||
*
|
||||
* @param request - Dados da requisição (mensagem, modelo, anexos, etc)
|
||||
* @returns Promise com a resposta da API
|
||||
*/
|
||||
async sendMessage(request: ChatMessageRequest): Promise<ChatResponse> {
|
||||
const {
|
||||
chatId,
|
||||
message,
|
||||
modelId,
|
||||
personalidade,
|
||||
anexos,
|
||||
userEmail,
|
||||
estabelecimentoId
|
||||
} = request;
|
||||
|
||||
// Usa valores do .env se não forem fornecidos
|
||||
const email = userEmail || import.meta.env.VITE_USER_EMAIL || '';
|
||||
const estabId = estabelecimentoId || import.meta.env.VITE_ESTABELECIMENTO_ID || '';
|
||||
|
||||
// Validações
|
||||
if (!chatId) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'ID do chat é obrigatório',
|
||||
};
|
||||
}
|
||||
|
||||
if (!message || message.trim().length === 0) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'A mensagem não pode estar vazia',
|
||||
};
|
||||
}
|
||||
|
||||
if (!modelId) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Modelo da IA é obrigatório',
|
||||
};
|
||||
}
|
||||
|
||||
// Valida número de anexos
|
||||
if (anexos && anexos.length > 5) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Máximo de 5 arquivos anexos permitidos',
|
||||
};
|
||||
}
|
||||
|
||||
// Cria FormData para envio multipart
|
||||
const formData = new FormData();
|
||||
formData.append('estabelecimento_id', estabId.toString());
|
||||
formData.append('chat_id', chatId);
|
||||
formData.append('user_email', email);
|
||||
formData.append('model_id', modelId);
|
||||
formData.append('message', message);
|
||||
|
||||
// Adiciona personalidade se fornecida
|
||||
if (personalidade && personalidade.trim().length > 0) {
|
||||
formData.append('personalidade', personalidade);
|
||||
}
|
||||
|
||||
// Adiciona anexos (máximo 5)
|
||||
if (anexos && anexos.length > 0) {
|
||||
anexos.forEach((file, index) => {
|
||||
if (index < 5) {
|
||||
formData.append(`anexo${index + 1}`, file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Log para debug (remover em produção se necessário)
|
||||
console.log('Enviando mensagem:', {
|
||||
chatId,
|
||||
messageLength: message.length,
|
||||
modelId,
|
||||
hasPersonalidade: !!personalidade,
|
||||
attachmentsCount: anexos?.length || 0,
|
||||
userEmail: email,
|
||||
estabelecimentoId: estabId,
|
||||
});
|
||||
|
||||
try {
|
||||
// Faz a requisição usando o serviço de API
|
||||
const response = await apiService.postFormData<ChatResponse>(
|
||||
this.CHAT_ENDPOINT,
|
||||
formData
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
// Trata erros específicos
|
||||
console.error('Erro ao enviar mensagem:', error);
|
||||
|
||||
throw {
|
||||
success: false,
|
||||
message: error.message || 'Erro ao comunicar com a IA',
|
||||
status: error.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva ou atualiza um chat no localStorage
|
||||
* Preparado para futura migração para banco de dados
|
||||
*
|
||||
* @param chat - Chat a ser salvo
|
||||
*/
|
||||
saveChat(chat: StoredChat): void {
|
||||
try {
|
||||
const chats = this.getAllChats();
|
||||
const existingIndex = chats.findIndex(c => c.id === chat.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
chats[existingIndex] = {
|
||||
...chat,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
} else {
|
||||
chats.push(chat);
|
||||
}
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEY_CHATS, JSON.stringify(chats));
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar chat:', error);
|
||||
throw new Error('Não foi possível salvar o chat');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca um chat específico por ID
|
||||
*
|
||||
* @param chatId - ID do chat
|
||||
* @returns Chat encontrado ou undefined
|
||||
*/
|
||||
getChat(chatId: string): StoredChat | undefined {
|
||||
const chats = this.getAllChats();
|
||||
return chats.find(c => c.id === chatId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna todos os chats salvos
|
||||
*
|
||||
* @returns Array de chats
|
||||
*/
|
||||
getAllChats(): StoredChat[] {
|
||||
try {
|
||||
const chatsJson = localStorage.getItem(this.STORAGE_KEY_CHATS);
|
||||
if (!chatsJson) return [];
|
||||
|
||||
const chats = JSON.parse(chatsJson);
|
||||
|
||||
// Converte strings de data para objetos Date
|
||||
return chats.map((chat: any) => ({
|
||||
...chat,
|
||||
createdAt: new Date(chat.createdAt),
|
||||
updatedAt: new Date(chat.updatedAt),
|
||||
messages: chat.messages.map((msg: any) => ({
|
||||
...msg,
|
||||
timestamp: new Date(msg.timestamp),
|
||||
})),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar chats:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleta um chat
|
||||
*
|
||||
* @param chatId - ID do chat a ser deletado
|
||||
*/
|
||||
deleteChat(chatId: string): void {
|
||||
try {
|
||||
const chats = this.getAllChats();
|
||||
const filteredChats = chats.filter(c => c.id !== chatId);
|
||||
localStorage.setItem(this.STORAGE_KEY_CHATS, JSON.stringify(filteredChats));
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar chat:', error);
|
||||
throw new Error('Não foi possível deletar o chat');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva uma pasta no localStorage
|
||||
*
|
||||
* @param folder - Pasta a ser salva
|
||||
*/
|
||||
saveFolder(folder: StoredFolder): void {
|
||||
try {
|
||||
const folders = this.getAllFolders();
|
||||
const existingIndex = folders.findIndex(f => f.id === folder.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
folders[existingIndex] = folder;
|
||||
} else {
|
||||
folders.push(folder);
|
||||
}
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEY_FOLDERS, JSON.stringify(folders));
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar pasta:', error);
|
||||
throw new Error('Não foi possível salvar a pasta');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna todas as pastas salvas
|
||||
*
|
||||
* @returns Array de pastas
|
||||
*/
|
||||
getAllFolders(): StoredFolder[] {
|
||||
try {
|
||||
const foldersJson = localStorage.getItem(this.STORAGE_KEY_FOLDERS);
|
||||
if (!foldersJson) return [];
|
||||
|
||||
const folders = JSON.parse(foldersJson);
|
||||
|
||||
// Converte strings de data para objetos Date
|
||||
return folders.map((folder: any) => ({
|
||||
...folder,
|
||||
createdAt: new Date(folder.createdAt),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar pastas:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleta uma pasta
|
||||
*
|
||||
* @param folderId - ID da pasta a ser deletada
|
||||
*/
|
||||
deleteFolder(folderId: string): void {
|
||||
try {
|
||||
const folders = this.getAllFolders();
|
||||
const filteredFolders = folders.filter(f => f.id !== folderId);
|
||||
localStorage.setItem(this.STORAGE_KEY_FOLDERS, JSON.stringify(filteredFolders));
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar pasta:', error);
|
||||
throw new Error('Não foi possível deletar a pasta');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera título automático para o chat baseado na primeira mensagem
|
||||
*
|
||||
* @param message - Primeira mensagem do usuário
|
||||
* @param maxLength - Comprimento máximo do título
|
||||
* @returns Título gerado
|
||||
*/
|
||||
generateChatTitle(message: string, maxLength: number = 50): string {
|
||||
if (!message || message.trim().length === 0) {
|
||||
return 'Nova Conversa';
|
||||
}
|
||||
|
||||
const trimmed = message.trim();
|
||||
if (trimmed.length <= maxLength) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return trimmed.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa todas as conversas (usar com cuidado)
|
||||
*/
|
||||
clearAllChats(): void {
|
||||
localStorage.removeItem(this.STORAGE_KEY_CHATS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa todas as pastas (usar com cuidado)
|
||||
*/
|
||||
clearAllFolders(): void {
|
||||
localStorage.removeItem(this.STORAGE_KEY_FOLDERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida anexos antes de enviar para a API
|
||||
*
|
||||
* @param files - Array de arquivos a serem validados
|
||||
* @returns Objeto com resultado da validação
|
||||
*/
|
||||
validateAttachments(files: File[]): { valid: boolean; error?: string } {
|
||||
// Valida número de anexos
|
||||
if (files.length > this.MAX_ATTACHMENTS) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Máximo de ${this.MAX_ATTACHMENTS} anexos permitidos. Você selecionou ${files.length}.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Valida cada arquivo
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const fileType = file.type;
|
||||
const fileName = file.name;
|
||||
|
||||
// Verifica se o tipo MIME é permitido
|
||||
if (!this.ALLOWED_FILE_TYPES[fileType as keyof typeof this.ALLOWED_FILE_TYPES]) {
|
||||
// Tenta validar pela extensão também
|
||||
const extension = fileName.toLowerCase().substring(fileName.lastIndexOf('.'));
|
||||
const isExtensionValid = Object.values(this.ALLOWED_FILE_TYPES).some(
|
||||
type => type.ext.includes(extension)
|
||||
);
|
||||
|
||||
if (!isExtensionValid) {
|
||||
const allowedFormats = Object.values(this.ALLOWED_FILE_TYPES)
|
||||
.map(t => t.label)
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
error: `Arquivo "${fileName}" não é permitido. Formatos aceitos: ${allowedFormats}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna lista de formatos de arquivo permitidos
|
||||
*
|
||||
* @returns Array com informações dos formatos
|
||||
*/
|
||||
getAllowedFileTypes() {
|
||||
return Object.entries(this.ALLOWED_FILE_TYPES).map(([mimeType, info]) => ({
|
||||
mimeType,
|
||||
...info,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna string formatada com formatos permitidos (para exibição)
|
||||
*
|
||||
* @returns String formatada (ex: "PDF, PNG, JPEG, WebP")
|
||||
*/
|
||||
getAllowedFileTypesLabel(): string {
|
||||
return Object.values(this.ALLOWED_FILE_TYPES)
|
||||
.map(t => t.label)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna string com extensões para usar no input file accept
|
||||
*
|
||||
* @returns String formatada (ex: ".pdf,.png,.jpg,.jpeg,.webp")
|
||||
*/
|
||||
getAllowedFileExtensions(): string {
|
||||
return Object.values(this.ALLOWED_FILE_TYPES)
|
||||
.map(t => t.ext)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna número máximo de anexos permitidos
|
||||
*/
|
||||
getMaxAttachments(): number {
|
||||
return this.MAX_ATTACHMENTS;
|
||||
}
|
||||
}
|
||||
|
||||
// Exporta instância única (Singleton)
|
||||
export const chatService = new ChatService();
|
||||
@@ -0,0 +1,218 @@
|
||||
import { apiService } from './api';
|
||||
|
||||
/**
|
||||
* Tamanhos de imagem disponíveis
|
||||
*/
|
||||
export type ImageSize = '1024x1024' | '1024x1792' | '1792x1024';
|
||||
|
||||
/**
|
||||
* Interface para a resposta da API de geração de imagens
|
||||
*/
|
||||
export interface ImageGenerationResponse {
|
||||
success: boolean;
|
||||
image_url: string; // URL da imagem gerada
|
||||
image_generation_id: string;
|
||||
message: string; // Descrição original
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface para os dados necessários para geração de imagem
|
||||
*/
|
||||
export interface ImageGenerationRequest {
|
||||
description: string;
|
||||
size: ImageSize;
|
||||
userEmail?: string;
|
||||
estabelecimentoId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Informações sobre cada tamanho de imagem disponível
|
||||
*/
|
||||
export const IMAGE_SIZE_OPTIONS = {
|
||||
'1024x1024': {
|
||||
label: 'Quadrado',
|
||||
dimensions: '1024x1024',
|
||||
aspectRatio: '1:1',
|
||||
description: 'Ideal para avatares, ícones e posts em redes sociais',
|
||||
},
|
||||
'1024x1792': {
|
||||
label: 'Retrato',
|
||||
dimensions: '1024x1792',
|
||||
aspectRatio: '9:16',
|
||||
description: 'Perfeito para stories, wallpapers verticais e reels',
|
||||
},
|
||||
'1792x1024': {
|
||||
label: 'Paisagem',
|
||||
dimensions: '1792x1024',
|
||||
aspectRatio: '16:9',
|
||||
description: 'Ótimo para banners, capas e thumbnails de vídeos',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Serviço de geração de imagens com IA
|
||||
*/
|
||||
class ImageGenerationService {
|
||||
private readonly IMAGE_GENERATION_ENDPOINT = '/webhook/codex/image_generator';
|
||||
|
||||
/**
|
||||
* Gera uma imagem a partir de uma descrição em texto
|
||||
*
|
||||
* @param request - Dados da requisição (descrição, tamanho, email, estabelecimento)
|
||||
* @returns Promise com a resposta da API
|
||||
*/
|
||||
async generateImage(request: ImageGenerationRequest): Promise<ImageGenerationResponse> {
|
||||
const { description, size, userEmail, estabelecimentoId } = request;
|
||||
|
||||
// Usa valores do .env se não forem fornecidos
|
||||
const email = userEmail || import.meta.env.VITE_USER_EMAIL || '';
|
||||
const estabId = estabelecimentoId || parseInt(import.meta.env.VITE_ESTABELECIMENTO_ID) || 1;
|
||||
|
||||
// Valida a descrição
|
||||
if (!description || description.trim().length === 0) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'A descrição não pode estar vazia',
|
||||
};
|
||||
}
|
||||
|
||||
// Valida o tamanho
|
||||
if (!this.isValidSize(size)) {
|
||||
throw {
|
||||
success: false,
|
||||
message: `Tamanho inválido. Opções disponíveis: ${Object.keys(IMAGE_SIZE_OPTIONS).join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Log para debug (remover em produção se necessário)
|
||||
console.log('Gerando imagem:', {
|
||||
descriptionLength: description.length,
|
||||
size,
|
||||
userEmail: email,
|
||||
estabelecimentoId: estabId,
|
||||
});
|
||||
|
||||
try {
|
||||
// Faz a requisição usando o serviço de API
|
||||
// Nota: O campo no body é "estabelecito_id" (com typo na API)
|
||||
const response = await apiService.post<ImageGenerationResponse>(
|
||||
this.IMAGE_GENERATION_ENDPOINT,
|
||||
{
|
||||
estabelecimento_id: estabId, // Mantém o typo da API original
|
||||
user_email: email,
|
||||
description: description,
|
||||
size: size,
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
// Trata erros específicos
|
||||
console.error('Erro na geração de imagem:', error);
|
||||
|
||||
throw {
|
||||
success: false,
|
||||
message: error.message || 'Erro ao gerar imagem',
|
||||
status: error.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se o tamanho selecionado é suportado
|
||||
*
|
||||
* @param size - Tamanho a ser validado
|
||||
* @returns true se o tamanho é válido
|
||||
*/
|
||||
isValidSize(size: string): size is ImageSize {
|
||||
return Object.keys(IMAGE_SIZE_OPTIONS).includes(size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém informações sobre um tamanho específico
|
||||
*
|
||||
* @param size - Tamanho da imagem
|
||||
* @returns Informações do tamanho
|
||||
*/
|
||||
getSizeInfo(size: ImageSize) {
|
||||
return IMAGE_SIZE_OPTIONS[size];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista todos os tamanhos disponíveis
|
||||
*
|
||||
* @returns Array com todas as opções de tamanho
|
||||
*/
|
||||
getAllSizes() {
|
||||
return Object.entries(IMAGE_SIZE_OPTIONS).map(([key, info]) => ({
|
||||
value: key as ImageSize,
|
||||
...info,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida a descrição para geração de imagem
|
||||
*
|
||||
* @param description - Descrição a ser validada
|
||||
* @param minLength - Comprimento mínimo (padrão: 3 caracteres)
|
||||
* @param maxLength - Comprimento máximo (padrão: 1000 caracteres)
|
||||
* @returns Objeto com resultado da validação
|
||||
*/
|
||||
validateDescription(
|
||||
description: string,
|
||||
minLength: number = 3,
|
||||
maxLength: number = 1000
|
||||
): { valid: boolean; error?: string } {
|
||||
if (!description || description.trim().length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'A descrição não pode estar vazia',
|
||||
};
|
||||
}
|
||||
|
||||
if (description.trim().length < minLength) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `A descrição deve ter pelo menos ${minLength} caracteres`,
|
||||
};
|
||||
}
|
||||
|
||||
if (description.length > maxLength) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `A descrição é muito longa. Máximo: ${maxLength} caracteres`,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Faz download de uma imagem gerada
|
||||
*
|
||||
* @param imageUrl - URL da imagem
|
||||
* @param filename - Nome do arquivo (opcional)
|
||||
*/
|
||||
async downloadImage(imageUrl: string, filename?: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename || `imagem_${Date.now()}.png`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Erro ao baixar imagem:', error);
|
||||
throw new Error('Não foi possível baixar a imagem');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exporta instância única (Singleton)
|
||||
export const imageGenerationService = new ImageGenerationService();
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Exporta todos os serviços de API
|
||||
*/
|
||||
|
||||
export { apiService } from './api';
|
||||
export { transcriptionService } from './transcription';
|
||||
export { audioGenerationService, VOICE_OPTIONS } from './audioGeneration';
|
||||
export { imageGenerationService, IMAGE_SIZE_OPTIONS } from './imageGeneration';
|
||||
export type { TranscriptionResponse, TranscriptionRequest } from './transcription';
|
||||
export type { AudioGenerationResponse, AudioGenerationRequest, VoiceType } from './audioGeneration';
|
||||
export type { ImageGenerationResponse, ImageGenerationRequest, ImageSize } from './imageGeneration';
|
||||
export type { ApiResponse, UserConfig, ApiError } from './types';
|
||||
@@ -0,0 +1,110 @@
|
||||
import { apiService } from './api';
|
||||
|
||||
/**
|
||||
* Interface para a resposta da API de transcrição
|
||||
*/
|
||||
export interface TranscriptionResponse {
|
||||
success: boolean;
|
||||
transcription_id: string;
|
||||
audio_url: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface para os dados necessários para transcrição
|
||||
*/
|
||||
export interface TranscriptionRequest {
|
||||
audioFile: File;
|
||||
userEmail?: string;
|
||||
estabelecimentoId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serviço de transcrição de áudio
|
||||
*/
|
||||
class TranscriptionService {
|
||||
private readonly TRANSCRIPTION_ENDPOINT = '/webhook/codex/transcrever_audio';
|
||||
|
||||
/**
|
||||
* Transcreve um arquivo de áudio
|
||||
*
|
||||
* @param request - Dados da requisição (arquivo, email, estabelecimento)
|
||||
* @returns Promise com a resposta da API
|
||||
*/
|
||||
async transcribeAudio(request: TranscriptionRequest): Promise<TranscriptionResponse> {
|
||||
const { audioFile, userEmail, estabelecimentoId } = request;
|
||||
|
||||
// Cria FormData para envio multipart
|
||||
const formData = new FormData();
|
||||
formData.append('data', audioFile);
|
||||
|
||||
// Usa valores do .env se não forem fornecidos
|
||||
const email = userEmail || import.meta.env.VITE_USER_EMAIL || '';
|
||||
const estabId = estabelecimentoId || import.meta.env.VITE_ESTABELECIMENTO_ID || '';
|
||||
|
||||
formData.append('user_email', email);
|
||||
formData.append('estabelecimento_id', estabId.toString());
|
||||
|
||||
// Log para debug (remover em produção se necessário)
|
||||
console.log('Enviando transcrição:', {
|
||||
fileName: audioFile.name,
|
||||
fileSize: audioFile.size,
|
||||
fileType: audioFile.type,
|
||||
userEmail: email,
|
||||
estabelecimentoId: estabId,
|
||||
});
|
||||
|
||||
try {
|
||||
// Faz a requisição usando o serviço de API
|
||||
const response = await apiService.postFormData<TranscriptionResponse>(
|
||||
this.TRANSCRIPTION_ENDPOINT,
|
||||
formData
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
// Trata erros específicos
|
||||
console.error('Erro na transcrição:', error);
|
||||
|
||||
throw {
|
||||
success: false,
|
||||
message: error.message || 'Erro ao transcrever áudio',
|
||||
status: error.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se o arquivo de áudio é suportado
|
||||
*
|
||||
* @param file - Arquivo a ser validado
|
||||
* @param maxSizeMB - Tamanho máximo em MB (padrão: 25MB)
|
||||
* @returns Objeto com resultado da validação
|
||||
*/
|
||||
validateAudioFile(file: File, maxSizeMB: number = 25): { valid: boolean; error?: string } {
|
||||
const SUPPORTED_FORMATS = ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm'];
|
||||
const MAX_FILE_SIZE = maxSizeMB * 1024 * 1024;
|
||||
|
||||
// Valida tamanho
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Arquivo muito grande. Tamanho máximo: ${maxSizeMB}MB`,
|
||||
};
|
||||
}
|
||||
|
||||
// Valida formato
|
||||
const fileExtension = file.name.split('.').pop()?.toLowerCase();
|
||||
if (!fileExtension || !SUPPORTED_FORMATS.includes(fileExtension)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Formato não suportado. Formatos aceitos: ${SUPPORTED_FORMATS.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Exporta instância única (Singleton)
|
||||
export const transcriptionService = new TranscriptionService();
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Tipos compartilhados para as APIs
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resposta padrão de sucesso/erro da API
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuração de usuário para requisições
|
||||
*/
|
||||
export interface UserConfig {
|
||||
userEmail?: string;
|
||||
estabelecimentoId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resposta de erro da API
|
||||
*/
|
||||
export interface ApiError {
|
||||
success: false;
|
||||
message: string;
|
||||
status?: number;
|
||||
data?: any;
|
||||
}
|
||||
Vendored
+11
@@ -1 +1,12 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL: string
|
||||
readonly VITE_API_KEY: string
|
||||
readonly VITE_USER_EMAIL: string
|
||||
readonly VITE_ESTABELECIMENTO_ID: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user