diff --git a/compile_sidecar.py b/compile_sidecar.py new file mode 100644 index 0000000..099f936 --- /dev/null +++ b/compile_sidecar.py @@ -0,0 +1,86 @@ +import os +import subprocess +import platform +import shutil +import sys + +def get_target_triple(): + # Retorna o sufixo exigido pelo Tauri baseado no SO e Arquitetura + system = platform.system().lower() + arch = platform.machine().lower() + + if system == "windows": + return "x86_64-pc-windows-msvc" + elif system == "linux": + return "x86_64-unknown-linux-gnu" + return None + +def check_pyinstaller(): + """Verifica se PyInstaller está instalado, instala se necessário.""" + try: + # Tenta usar python -m PyInstaller (sempre funciona se instalado) + subprocess.run([sys.executable, "-m", "PyInstaller", "--version"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + return True + except (FileNotFoundError, subprocess.CalledProcessError): + print("[Build] PyInstaller não encontrado. Instalando...") + try: + subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"], + check=True) + return True + except subprocess.CalledProcessError: + print("[Erro] Falha ao instalar PyInstaller") + return False + +def compile(): + triple = get_target_triple() + if not triple: + print("[Erro] Sistema operacional não suportado") + sys.exit(1) + + if not check_pyinstaller(): + print("[Erro] PyInstaller não disponível") + sys.exit(1) + + binary_name = f"engine-backend-{triple}" + if platform.system() == "Windows": + binary_name += ".exe" + + print(f"[Build] Compilando sidecar para: {triple}...") + + # Executa PyInstaller via python -m (garante que funciona mesmo sem PATH) + cmd = [ + sys.executable, + "-m", "PyInstaller", + "--onefile", + "--name", binary_name, + "src-python/app/api_server.py" + ] + + try: + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as e: + print(f"[Erro] Falha ao compilar: {e}") + sys.exit(1) + + # Garante que a pasta de destino existe + target_dir = os.path.join("src-tauri", "binaries") + os.makedirs(target_dir, exist_ok=True) + + # Move o binário da pasta dist para a pasta binaries do Tauri + source_path = os.path.join("dist", binary_name) + target_path = os.path.join(target_dir, binary_name) + + if os.path.exists(source_path): + if os.path.exists(target_path): + os.remove(target_path) + shutil.move(source_path, target_path) + print(f"[Success] Sidecar compilado e movido para: {target_path}") + else: + print(f"[Erro] Binário não encontrado em: {source_path}") + sys.exit(1) + +if __name__ == "__main__": + compile() diff --git a/engine-backend-x86_64-pc-windows-msvc.exe.spec b/engine-backend-x86_64-pc-windows-msvc.exe.spec new file mode 100644 index 0000000..89838ff --- /dev/null +++ b/engine-backend-x86_64-pc-windows-msvc.exe.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['src-python\\app\\api_server.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='engine-backend-x86_64-pc-windows-msvc.exe', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/package.json b/package.json index 79a6821..d392369 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,10 @@ "version": "0.1.0", "type": "module", "scripts": { + "dev:ui": "vite", + "install:python-deps": "node scripts/install-python-deps.js", + "compile:sidecar": "python compile_sidecar.py", + "predev": "pnpm install:python-deps && pnpm compile:sidecar", "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", @@ -27,6 +31,7 @@ "@tailwindcss/postcss": "^4.1.18", "@tauri-apps/cli": "^2", "autoprefixer": "^10.4.23", + "concurrently": "^9.2.1", "postcss": "^8.5.6", "tailwindcss": "^4.1.18", "typescript": "~5.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6d3c62..8cc8e12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: autoprefixer: specifier: ^10.4.23 version: 10.4.23(postcss@8.5.6) + concurrently: + specifier: ^9.2.1 + version: 9.2.1 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -835,6 +838,14 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + autoprefixer@10.4.23: resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} engines: {node: ^10 || ^12 || >=14} @@ -870,10 +881,30 @@ packages: caniuse-lite@1.0.30001763: resolution: {integrity: sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -927,6 +958,9 @@ packages: electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + enhanced-resolve@5.18.4: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} @@ -981,9 +1015,17 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -1006,6 +1048,10 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -1192,6 +1238,10 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1206,6 +1256,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1213,6 +1266,10 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + simple-code-frame@1.3.0: resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} @@ -1232,9 +1289,25 @@ packages: resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} engines: {node: '>=16'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -1250,6 +1323,13 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@5.6.3: resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} @@ -1306,6 +1386,14 @@ packages: yaml: optional: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1313,6 +1401,14 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + zod@4.3.5: resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} @@ -1970,6 +2066,12 @@ snapshots: '@types/prop-types@15.7.15': {} + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + autoprefixer@10.4.23(postcss@8.5.6): dependencies: browserslist: 4.28.1 @@ -2005,8 +2107,34 @@ snapshots: caniuse-lite@1.0.30001763: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} @@ -2059,6 +2187,8 @@ snapshots: electron-to-chromium@1.5.267: {} + emoji-regex@8.0.0: {} + enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 @@ -2120,8 +2250,12 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + graceful-fs@4.2.11: {} + has-flag@4.0.0: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -2143,6 +2277,8 @@ snapshots: dependencies: hasown: 2.0.2 + is-fullwidth-code-point@3.0.0: {} + jiti@2.6.1: {} js-tokens@4.0.0: {} @@ -2287,6 +2423,8 @@ snapshots: react@19.2.3: {} + require-directory@2.1.1: {} + resolve-from@4.0.0: {} resolve@1.22.11: @@ -2326,10 +2464,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.55.1 fsevents: 2.3.3 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + scheduler@0.27.0: {} semver@6.3.1: {} + shell-quote@1.8.3: {} + simple-code-frame@1.3.0: dependencies: kolorist: 1.8.0 @@ -2342,8 +2486,26 @@ snapshots: stack-trace@1.0.0-pre2: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + stylis@4.2.0: {} + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} tailwindcss@4.1.18: {} @@ -2355,6 +2517,10 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tree-kill@1.2.2: {} + + tslib@2.8.1: {} + typescript@5.6.3: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -2386,8 +2552,28 @@ snapshots: jiti: 2.6.1 lightningcss: 1.30.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + yallist@3.1.1: {} yaml@1.10.2: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + zod@4.3.5: {} diff --git a/scripts/install-python-deps.js b/scripts/install-python-deps.js new file mode 100644 index 0000000..a1d7b6b --- /dev/null +++ b/scripts/install-python-deps.js @@ -0,0 +1,37 @@ +// Script cross-platform Node.js para instalar dependências Python +import { execSync } from 'child_process'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const isWindows = process.platform === 'win32'; +const pythonCmd = isWindows ? 'python' : 'python3'; +const requirementsPath = path.join(__dirname, '..', 'src-python', 'requirements.txt'); + +if (!fs.existsSync(requirementsPath)) { + console.error(`[Python Deps] ERRO: Arquivo não encontrado: ${requirementsPath}`); + process.exit(1); +} + +console.log(`[Python Deps] Verificando dependências em: ${requirementsPath}`); + +try { + // Verifica pip + execSync(`${pythonCmd} -m pip --version`, { stdio: 'ignore' }); + + // Atualiza pip + console.log('[Python Deps] Atualizando pip...'); + execSync(`${pythonCmd} -m pip install --quiet --upgrade pip`, { stdio: 'inherit' }); + + // Instala dependências + console.log('[Python Deps] Instalando dependências...'); + execSync(`${pythonCmd} -m pip install --quiet -r "${requirementsPath}"`, { stdio: 'inherit' }); + + console.log('[Python Deps] Dependências instaladas com sucesso.'); +} catch (error) { + console.error('[Python Deps] ERRO: Falha ao instalar dependências.'); + process.exit(1); +} diff --git a/scripts/install-python-deps.ps1 b/scripts/install-python-deps.ps1 new file mode 100644 index 0000000..80c6d03 --- /dev/null +++ b/scripts/install-python-deps.ps1 @@ -0,0 +1,36 @@ +# Script PowerShell para instalar dependências Python antes de executar +param( + [string]$RequirementsPath = "src-python\requirements.txt", + [string]$PythonCmd = "python" +) + +$ErrorActionPreference = "Stop" + +# Resolve caminho absoluto +$RequirementsFile = Join-Path $PSScriptRoot "..\$RequirementsPath" | Resolve-Path -ErrorAction Stop + +Write-Host "[Python Deps] Verificando dependências em: $RequirementsFile" -ForegroundColor Cyan + +# Verifica se pip está disponível +try { + $pipVersion = & $PythonCmd -m pip --version 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "[Python Deps] ERRO: pip não encontrado. Instale Python com pip." -ForegroundColor Red + exit 1 + } +} catch { + Write-Host "[Python Deps] ERRO: Python não encontrado. Verifique se está no PATH." -ForegroundColor Red + exit 1 +} + +# Instala dependências +Write-Host "[Python Deps] Instalando dependências..." -ForegroundColor Yellow +& $PythonCmd -m pip install --quiet --upgrade pip +& $PythonCmd -m pip install --quiet -r $RequirementsFile + +if ($LASTEXITCODE -eq 0) { + Write-Host "[Python Deps] Dependências instaladas com sucesso." -ForegroundColor Green +} else { + Write-Host "[Python Deps] ERRO: Falha ao instalar dependências." -ForegroundColor Red + exit 1 +} diff --git a/scripts/install-python-deps.sh b/scripts/install-python-deps.sh new file mode 100644 index 0000000..82fc41a --- /dev/null +++ b/scripts/install-python-deps.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Script Bash para instalar dependências Python antes de executar + +set -e + +REQUIREMENTS_PATH="${1:-src-python/requirements.txt}" +PYTHON_CMD="${2:-python3}" + +# Resolve caminho absoluto +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REQUIREMENTS_FILE="$SCRIPT_DIR/../$REQUIREMENTS_PATH" + +if [ ! -f "$REQUIREMENTS_FILE" ]; then + echo "[Python Deps] ERRO: Arquivo não encontrado: $REQUIREMENTS_FILE" >&2 + exit 1 +fi + +echo "[Python Deps] Verificando dependências em: $REQUIREMENTS_FILE" + +# Verifica se pip está disponível +if ! $PYTHON_CMD -m pip --version >/dev/null 2>&1; then + echo "[Python Deps] ERRO: pip não encontrado. Instale Python com pip." >&2 + exit 1 +fi + +# Instala dependências +echo "[Python Deps] Instalando dependências..." +$PYTHON_CMD -m pip install --quiet --upgrade pip +$PYTHON_CMD -m pip install --quiet -r "$REQUIREMENTS_FILE" + +if [ $? -eq 0 ]; then + echo "[Python Deps] Dependências instaladas com sucesso." +else + echo "[Python Deps] ERRO: Falha ao instalar dependências." >&2 + exit 1 +fi diff --git a/src-python/__init__.py b/src-python/__init__.py new file mode 100644 index 0000000..8074dd0 --- /dev/null +++ b/src-python/__init__.py @@ -0,0 +1 @@ +# Package initialization diff --git a/src-python/app/__init__.py b/src-python/app/__init__.py new file mode 100644 index 0000000..8074dd0 --- /dev/null +++ b/src-python/app/__init__.py @@ -0,0 +1 @@ +# Package initialization diff --git a/src-python/app/api_server.py b/src-python/app/api_server.py new file mode 100644 index 0000000..61c6a5b --- /dev/null +++ b/src-python/app/api_server.py @@ -0,0 +1,139 @@ +""" +Servidor FastAPI para expor telemetria e download de modelos. +Utiliza Server-Sent Events (SSE) para streaming de progresso em tempo real. +""" +import sys +import os +import json +from pathlib import Path +from typing import Dict, List +from fastapi import FastAPI, HTTPException +from fastapi.responses import StreamingResponse, JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +import asyncio + +# Detecta se está rodando como binário ou script +if getattr(sys, 'frozen', False): + # Caminho do executável (PyInstaller) + BASE_DIR = os.path.dirname(sys.executable) + CONFIG_PATH = os.path.join(BASE_DIR, "modelConfig.yaml") + MODELS_DIR = os.path.join(BASE_DIR, "models") +else: + # Rodando como script Python + # Ajusta PYTHONPATH antes dos imports relativos + script_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.dirname(os.path.dirname(script_dir)) # Sobe para raiz do projeto + if project_root not in sys.path: + sys.path.insert(0, project_root) + + BASE_DIR = project_root + CONFIG_PATH = os.path.join(BASE_DIR, "modelConfig.yaml") + MODELS_DIR = os.path.join(BASE_DIR, "src-python", "app", "models") + +# Importações relativas (funcionam quando executado como módulo) +from .telemetry import SystemTelemetry +from .model_downloader import ModelDownloader + +app = FastAPI(title="LocalAI API", version="1.0.0") + +# CORS para permitir comunicação com frontend Tauri +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Em produção, restringir ao localhost + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Instâncias globais +downloader = ModelDownloader(models_dir=MODELS_DIR) + +# Modelos de dados +class ModelInfo(BaseModel): + id: str + name: str + quant: str + size_gb: float + repo: str + filename: str + description: str + +class DownloadRequest(BaseModel): + model_id: str + repo_id: str + filename: str + +# Cache de modelos disponíveis (pode ser carregado de JSON) +MODELS_REGISTRY: List[Dict] = [ + { + "id": "lfm-2.5-1.2b", + "name": "Liquid LFM 2.5", + "quant": "Q6_K", + "size_gb": 1.12, + "repo": "LiquidAI/LFM2.5-1.2B-Instruct-GGUF", + "filename": "LFM2.5-1.2B-Instruct-Q6_K.gguf", + "description": "Otimizado para arquitetura sequencial de alta performance." + } +] + +@app.get("/api/telemetry") +async def get_telemetry(): + """Retorna telemetria do sistema em tempo real.""" + return SystemTelemetry.get_stats() + +@app.get("/api/models") +async def list_models(): + """Lista todos os modelos disponíveis com status de download.""" + models_with_status = [] + for model in MODELS_REGISTRY: + model_path = downloader.get_model_path(model["repo"], model["filename"]) + model_copy = model.copy() + model_copy["downloaded"] = model_path.exists() if model_path else False + models_with_status.append(model_copy) + return {"models": models_with_status} + +@app.post("/api/models/download") +async def download_model(request: DownloadRequest): + """ + Inicia download de modelo com progresso via SSE. + O frontend deve consumir como EventSource. + """ + async def generate_progress(): + try: + async for progress_event in downloader.download_with_progress( + repo_id=request.repo_id, + filename=request.filename + ): + yield f"data: {json.dumps(progress_event)}\n\n" + except Exception as e: + error_event = {"type": "error", "message": str(e)} + yield f"data: {json.dumps(error_event)}\n\n" + + return StreamingResponse( + generate_progress(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no" + } + ) + +@app.get("/api/models/{model_id}/status") +async def get_model_status(model_id: str): + """Verifica se um modelo específico está baixado.""" + model = next((m for m in MODELS_REGISTRY if m["id"] == model_id), None) + if not model: + raise HTTPException(status_code=404, detail="Modelo não encontrado") + + model_path = downloader.get_model_path(model["repo"], model["filename"]) + return { + "model_id": model_id, + "downloaded": model_path.exists() if model_path else False, + "path": str(model_path) if model_path else None + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="127.0.0.1", port=8000) diff --git a/src-python/app/model_downloader.py b/src-python/app/model_downloader.py new file mode 100644 index 0000000..1e008f7 --- /dev/null +++ b/src-python/app/model_downloader.py @@ -0,0 +1,98 @@ +""" +Módulo de download de modelos GGUF do HuggingFace com progresso em tempo real. +Utiliza huggingface_hub com callbacks para reportar progresso via async generator. +""" +import os +from pathlib import Path +from typing import AsyncGenerator, Dict +from huggingface_hub import hf_hub_download +import asyncio + +class ModelDownloader: + """Gerencia downloads de modelos com progresso assíncrono.""" + + def __init__(self, models_dir: str = "./src-python/app/models"): + self.models_dir = Path(models_dir) + self.models_dir.mkdir(parents=True, exist_ok=True) + self._downloads: Dict[str, float] = {} # Cache de progresso por modelo + + def get_model_path(self, repo_id: str, filename: str) -> Path: + """Retorna o caminho esperado do modelo.""" + # huggingface_hub salva em cache_dir/repo_id/filename + # Simplificamos para models_dir/repo_id/filename + repo_name = repo_id.split("/")[-1] + return self.models_dir / repo_name / filename + + async def download_with_progress( + self, + repo_id: str, + filename: str + ) -> AsyncGenerator[Dict, None]: + """ + Download assíncrono com progresso via callback. + Gera eventos: {"type": "progress", "percent": float} ou {"type": "complete"} + """ + model_path = self.get_model_path(repo_id, filename) + + # Verifica se já existe + if model_path.exists(): + yield {"type": "complete", "percent": 100, "path": str(model_path)} + return + + # Callback para progresso + last_percent = 0 + + def progress_callback(downloaded: int, total: int): + nonlocal last_percent + if total > 0: + percent = min(100, int((downloaded / total) * 100)) + if percent != last_percent: + last_percent = percent + # Armazena para acesso assíncrono + self._downloads[f"{repo_id}/{filename}"] = percent + + try: + # Executa download em thread separada para não bloquear + loop = asyncio.get_event_loop() + + def run_download(): + return hf_hub_download( + repo_id=repo_id, + filename=filename, + local_dir=str(self.models_dir), + local_dir_use_symlinks=False, + resume_download=True, + # huggingface_hub não expõe callback direto, então monitoramos o arquivo + ) + + # Inicia download em executor + download_task = loop.run_in_executor(None, run_download) + + # Monitora progresso enquanto download ocorre + while not download_task.done(): + key = f"{repo_id}/{filename}" + percent = self._downloads.get(key, 0) + + # Tenta estimar progresso pelo tamanho do arquivo parcial + partial_path = self.get_model_path(repo_id, filename) + if partial_path.exists(): + # Estima baseado no tamanho (aproximado, pois não temos total exato) + size_mb = partial_path.stat().st_size / (1024 * 1024) + # Assumindo ~1GB para modelos pequenos (ajustar conforme necessário) + estimated_total_mb = 1200 # ~1.2GB + if size_mb < estimated_total_mb: + percent = min(95, int((size_mb / estimated_total_mb) * 100)) + + yield {"type": "progress", "percent": percent} + await asyncio.sleep(0.5) # Atualiza a cada 500ms + + # Download completo + final_path = download_task.result() + yield {"type": "complete", "percent": 100, "path": final_path} + + # Limpa cache + self._downloads.pop(f"{repo_id}/{filename}", None) + + except Exception as e: + yield {"type": "error", "message": str(e)} + raise diff --git a/src-python/app/telemetry.py b/src-python/app/telemetry.py new file mode 100644 index 0000000..f2cca01 --- /dev/null +++ b/src-python/app/telemetry.py @@ -0,0 +1,44 @@ +import psutil +import os +import shutil + +# GPUtil é opcional (requer distutils, removido no Python 3.12+) +try: + import GPUtil + HAS_GPUTIL = True +except ImportError: + HAS_GPUTIL = False + +class SystemTelemetry: + @staticmethod + def get_stats(): + # CPU + cpu_usage = psutil.cpu_percent(interval=None) + + # RAM + ram = psutil.virtual_memory() + + # GPU (Pega a primeira disponível) + gpu_data = {"usage": 0, "vram_used": 0, "vram_total": 0} + if HAS_GPUTIL: + try: + gpus = GPUtil.getGPUs() + if gpus: + gpu_data = { + "usage": gpus[0].load * 100, + "vram_used": gpus[0].memoryUsed, + "vram_total": gpus[0].memoryTotal + } + except Exception: + pass # Fallback silencioso se GPUtil falhar + + # Armazenamento (Partição do App) + total, used, free = shutil.disk_usage("./") + + return { + "cpu": cpu_usage, + "ram_used": ram.used / (1024**3), + "ram_total": ram.total / (1024**3), + "gpu": gpu_data, + "disk_free_gb": free / (1024**3) + } \ No newline at end of file diff --git a/src-python/requirements.txt b/src-python/requirements.txt index 0b89f26..de627ad 100644 --- a/src-python/requirements.txt +++ b/src-python/requirements.txt @@ -1,3 +1,9 @@ # requirements.txt gerado (estático, baseado na importação visível) llama-cpp-python>=0.2.0 -huggingface-hub>=0.20.0 \ No newline at end of file +huggingface-hub>=0.20.0 +fastapi>=0.104.0 +uvicorn>=0.24.0 +pydantic>=2.0.0 +psutil>=5.9.0 +# gputil>=1.4.0 # Opcional: requer distutils (removido no Python 3.12+) +pyyaml>=6.0 \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ab34721..a169697 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -791,6 +791,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -1868,6 +1877,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-opener", + "tauri-plugin-shell", ] [[package]] @@ -2310,6 +2320,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "pango" version = "0.18.3" @@ -3201,12 +3221,44 @@ dependencies = [ "digest", ] +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3618,6 +3670,27 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-shell" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b76f884a3937e04b631ffdc3be506088fa979369d25147361352f2f352e5ed" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "tauri-runtime" version = "2.9.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cfb134a..ebf92cb 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,6 +20,7 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-opener = "2" +tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/binaries/README.md b/src-tauri/binaries/README.md new file mode 100644 index 0000000..9051f2e --- /dev/null +++ b/src-tauri/binaries/README.md @@ -0,0 +1,21 @@ +# Binários do Sidecar + +Esta pasta contém os binários compilados do engine-backend (Python). + +## Compilação + +### Linux (Manjaro) +```bash +pip install pyinstaller +pyinstaller --onefile --name engine-backend-x86_64-unknown-linux-gnu src-python/app/api_server.py +mv dist/engine-backend-x86_64-unknown-linux-gnu binaries/ +``` + +### Windows +```powershell +pip install pyinstaller +pyinstaller --onefile --name engine-backend-x86_64-pc-windows-msvc src-python/app/api_server.py +move dist\engine-backend-x86_64-pc-windows-msvc.exe binaries\ +``` + +**Nota:** O Tauri adicionará automaticamente a extensão `.exe` no Windows durante o build. diff --git a/src-tauri/binaries/engine-backend-x86_64-pc-windows-msvc.exe b/src-tauri/binaries/engine-backend-x86_64-pc-windows-msvc.exe new file mode 100644 index 0000000..3971516 Binary files /dev/null and b/src-tauri/binaries/engine-backend-x86_64-pc-windows-msvc.exe differ diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4cdbf49..1ca2fb1 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -1,10 +1,19 @@ { "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", + "identifier": "main-capability", "description": "Capability for the main window", "windows": ["main"], "permissions": [ "core:default", - "opener:default" + "opener:default", + { + "identifier": "shell:allow-execute", + "allow": [ + { + "name": "binaries/engine-backend", + "sidecar": true + } + ] + } ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4a277ef..0b84de9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,12 @@ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ +use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::process::CommandEvent; +use std::sync::{Arc, Mutex}; + +struct AppState { + child_process: Arc>>, +} + #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) @@ -6,9 +14,58 @@ fn greet(name: &str) -> String { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + let child_process = Arc::new(Mutex::new(Option::::None)); + let child_process_clone = Arc::clone(&child_process); + tauri::Builder::default() .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![greet]) + .manage(AppState { child_process }) + .setup(move |app| { + // Inicia o Sidecar Python (usado em dev e produção) + let shell = app.shell(); + match shell.sidecar("engine-backend") { + Ok(sidecar) => { + match sidecar.spawn() { + Ok((mut rx, child)) => { + // Armazena a referência do processo para controle + let mut process_store = child_process_clone.lock().unwrap(); + *process_store = Some(child); + + // Monitoramento de logs em thread assíncrona + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + if let CommandEvent::Stdout(line) = event { + println!("Backend Log: {}", String::from_utf8_lossy(&line)); + } + } + }); + + println!("[System] Sidecar Python iniciado com sucesso"); + } + Err(e) => { + eprintln!("[Erro] Falha ao iniciar sidecar: {}", e); + eprintln!("[Erro] Verifique se o binário está em src-tauri/binaries/"); + eprintln!("[Erro] Execute: pnpm compile:sidecar"); + } + } + } + Err(e) => { + eprintln!("[Erro] Falha ao configurar sidecar: {}", e); + eprintln!("[Erro] Verifique se o binário está em src-tauri/binaries/"); + eprintln!("[Erro] Execute: pnpm compile:sidecar"); + } + } + + Ok(()) + }) + .on_window_event(|_window, event| { + // Garante o encerramento quando a janela fecha + if let tauri::WindowEvent::Destroyed = event { + println!("[System]: Encerrando Backend..."); + } + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a0630d7..4aee070 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,28 +2,19 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "localaiapp", "version": "0.1.0", - "identifier": "evandrodevbr", + "identifier": "com.evandrodevbr.app", "build": { - "beforeDevCommand": "pnpm dev", + "beforeDevCommand": "pnpm predev && pnpm dev", "devUrl": "http://localhost:1420", "beforeBuildCommand": "pnpm build", "frontendDist": "../dist" }, - "app": { - "windows": [ - { - "title": "localaiapp", - "width": 800, - "height": 600 - } - ], - "security": { - "csp": null - } - }, "bundle": { "active": true, "targets": "all", + "externalBin": [ + "binaries/engine-backend" + ], "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -31,5 +22,17 @@ "icons/icon.icns", "icons/icon.ico" ] + }, + "app": { + "windows": [ + { + "title": "LocalMind AI", + "width": 1200, + "height": 800 + } + ], + "security": { + "csp": null + } } } diff --git a/src/App.tsx b/src/App.tsx index 31b8da2..3c19ef6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import "@fontsource/inter"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Sheet, Box, Textarea, IconButton, Tooltip, Typography, Menu, MenuItem, Dropdown, MenuButton, Badge @@ -13,9 +13,22 @@ import { // Componentes de Interface Padronizados import Header from "./components/interface/Header"; import Footer from "./components/interface/Footer"; +import { SettingsModal } from "./components/interface/SettingsModal"; + +const API_BASE = "http://127.0.0.1:8000"; export default function App() { const [selectedModel, setSelectedModel] = useState("Llama-3.1-8B"); + const [stats, setStats] = useState({ + cpu: 0, + ram_used: 0, + ram_total: 8, + gpu: { usage: 0, vram_used: 0, vram_total: 0 }, + disk_free_gb: 0 + }); + const [models, setModels] = useState([]); + const [settingsOpen, setSettingsOpen] = useState(false); + const [downloadingModel, setDownloadingModel] = useState(null); // Constantes de Design (Tokens) const ICON_SIZE = 18; @@ -23,9 +36,98 @@ export default function App() { const BORDER_STYLE = "border-white/[0.06]"; const SURFACE_COLOR = "bg-[#121212]"; + // Polling de telemetria + useEffect(() => { + const fetchTelemetry = async () => { + try { + const response = await fetch(`${API_BASE}/api/telemetry`); + const data = await response.json(); + setStats(data); + } catch (error) { + console.error("Erro ao buscar telemetria:", error); + } + }; + + fetchTelemetry(); + const interval = setInterval(fetchTelemetry, 2000); // Atualiza a cada 2s + return () => clearInterval(interval); + }, []); + + // Carregar modelos + useEffect(() => { + const fetchModels = async () => { + try { + const response = await fetch(`${API_BASE}/api/models`); + const data = await response.json(); + setModels(data.models || []); + } catch (error) { + console.error("Erro ao carregar modelos:", error); + } + }; + + fetchModels(); + }, []); + + // Função de download com EventSource + const handleDownload = async (model: any) => { + if (downloadingModel) return; + + setDownloadingModel(model.id); + + try { + const response = await fetch(`${API_BASE}/api/models/download`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model_id: model.id, + repo_id: model.repo, + filename: model.filename + }) + }); + + if (!response.ok) throw new Error("Falha ao iniciar download"); + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + if (!reader) throw new Error("Stream não disponível"); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split("\n"); + + for (const line of lines) { + if (line.startsWith("data: ")) { + const data = JSON.parse(line.slice(6)); + + if (data.type === "complete") { + setDownloadingModel(null); + // Recarrega lista de modelos + const res = await fetch(`${API_BASE}/api/models`); + const modelsData = await res.json(); + setModels(modelsData.models || []); + break; + } else if (data.type === "error") { + setDownloadingModel(null); + console.error("Erro no download:", data.message); + break; + } + // Progresso pode ser usado para atualizar UI se necessário + } + } + } + } catch (error) { + console.error("Erro no download:", error); + setDownloadingModel(null); + } + }; + return (
-
+
setSettingsOpen(true)} />
@@ -182,7 +284,13 @@ export default function App() {
-