feat: enhance application functionality and telemetry monitoring
- Added new scripts in `package.json` for Python dependency installation and sidecar compilation. - Integrated telemetry fetching in `src/App.tsx` to monitor CPU, RAM, GPU, and disk usage. - Implemented model downloading functionality with progress tracking in `src/App.tsx`. - Updated `Footer` and `Header` components to display telemetry data and settings options. - Enhanced Tauri configuration for sidecar execution and application window settings. - Updated Python requirements in `requirements.txt` to include additional dependencies for improved functionality.
This commit is contained in:
1 parent
482b28513e
commit
08e154894a
24 files changed
+1110
-91
No files matched your search
@@ -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()
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
Generated
+186
@@ -54,6 +54,9 @@ importers:
|
||||
autoprefixer:
|
||||
specifier: ^10.4.23
|
||||
version: 10.4.23([email protected])
|
||||
concurrently:
|
||||
specifier: ^9.2.1
|
||||
version: 9.2.1
|
||||
postcss:
|
||||
specifier: ^8.5.6
|
||||
version: 8.5.6
|
||||
@@ -835,6 +838,14 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -870,10 +881,30 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
|
||||
|
||||
@@ -927,6 +958,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
[email protected]:
|
||||
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'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1006,6 +1048,10 @@ packages:
|
||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
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'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==}
|
||||
|
||||
@@ -1232,9 +1289,25 @@ packages:
|
||||
resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1250,6 +1323,13 @@ packages:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -1306,6 +1386,14 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
@@ -1313,6 +1401,14 @@ packages:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
|
||||
|
||||
@@ -1970,6 +2066,12 @@ snapshots:
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
@@ -2005,8 +2107,34 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -2059,6 +2187,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -2120,8 +2250,12 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
@@ -2143,6 +2277,8 @@ snapshots:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -2287,6 +2423,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -2326,10 +2464,16 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.55.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
kolorist: 1.8.0
|
||||
@@ -2342,8 +2486,26 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -2355,6 +2517,10 @@ snapshots:
|
||||
fdir: 6.5.0([email protected])
|
||||
picomatch: 4.0.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]):
|
||||
@@ -2386,8 +2552,28 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.30.2
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]: {}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
# Package initialization
|
||||
@@ -0,0 +1 @@
|
||||
# Package initialization
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
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
|
||||
Generated
+73
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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<Mutex<Option<tauri_plugin_shell::process::CommandChild>>>,
|
||||
}
|
||||
|
||||
#[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::<tauri_plugin_shell::process::CommandChild>::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");
|
||||
}
|
||||
+17
-14
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
-3
@@ -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<any[]>([]);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [downloadingModel, setDownloadingModel] = useState<string | null>(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 (
|
||||
<div className="flex flex-col h-screen bg-[#0A0A0A] text-slate-200 overflow-hidden font-inter selection:bg-cyan-500/30">
|
||||
<Header />
|
||||
<Header onSettingsClick={() => setSettingsOpen(true)} />
|
||||
|
||||
<main className="flex flex-1 overflow-hidden border-t border-white/[0.05]">
|
||||
|
||||
@@ -182,7 +284,13 @@ export default function App() {
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
<Footer stats={stats} />
|
||||
<SettingsModal
|
||||
open={settingsOpen}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
models={models}
|
||||
onDownload={handleDownload}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +1,58 @@
|
||||
import "@fontsource/inter";
|
||||
import { Typography, Divider, Link, Box } from "@mui/joy";
|
||||
import { Github, Database, Cpu, ShieldCheck, Terminal } from "lucide-react";
|
||||
import { LinearProgress, Typography, Box, Tooltip } from "@mui/joy";
|
||||
import { Cpu, Database, HardDrive, Activity } from "lucide-react";
|
||||
|
||||
export default function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const modelName = "Llama-3.1-8B-Q4_K_M.gguf"; // Exemplo técnico
|
||||
// Componente de Telemetria Interno
|
||||
const StatMonitor = ({ icon: Icon, label, value, percent, color }: any) => (
|
||||
<Box sx={{ width: 120 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography level="body-xs" sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: 'slate.400' }}>
|
||||
<Icon size={12} /> {label}
|
||||
</Typography>
|
||||
<Typography level="body-xs" sx={{ color: 'white', fontWeight: 'bold' }}>{value}</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
determinate
|
||||
value={percent}
|
||||
size="sm"
|
||||
color={color}
|
||||
sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default function EnhancedFooter({ stats }: { stats: any }) {
|
||||
return (
|
||||
<footer className="w-full bg-[#0A0A0A] border-t border-white/[0.05] px-6 py-4">
|
||||
{/* Container Principal */}
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
|
||||
{/* Lado Esquerdo: Metadados do App */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]"></div>
|
||||
<Typography level="body-xs" sx={{ color: 'white', fontWeight: 600, fontFamily: 'Inter' }}>
|
||||
SYSTEM SECURE
|
||||
</Typography>
|
||||
</div>
|
||||
<Divider orientation="vertical" sx={{ height: '14px', bgcolor: 'white/10' }} />
|
||||
<Typography level="body-xs" sx={{ color: 'rgba(255,255,255,0.4)', fontFamily: 'Inter' }}>
|
||||
© {currentYear} Evandro Fonseca Jr.
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
{/* Centro: Telemetria da IA (Onde o engenheiro foca) */}
|
||||
<div className="flex items-center gap-6 px-4 py-1.5 bg-white/[0.02] border border-white/[0.05] rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database size={14} className="text-cyan-500" />
|
||||
<span className="text-[11px] font-mono text-slate-400 uppercase tracking-wider">Model:</span>
|
||||
<span className="text-[11px] font-mono text-slate-200">{modelName}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu size={14} className="text-purple-500" />
|
||||
<span className="text-[11px] font-mono text-slate-400 uppercase tracking-wider">VRAM:</span>
|
||||
<span className="text-[11px] font-mono text-slate-200">5.2 GB / 8.0 GB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lado Direito: Links Técnicos e Segurança */}
|
||||
<div className="flex items-center gap-5">
|
||||
<Link
|
||||
href="#"
|
||||
startDecorator={<Terminal size={14} />}
|
||||
sx={{
|
||||
fontSize: '12px',
|
||||
color: 'rgba(255,255,255,0.5)',
|
||||
textDecoration: 'none',
|
||||
'&:hover': { color: 'white' }
|
||||
}}
|
||||
>
|
||||
API Logs
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
sx={{ color: 'rgba(255,255,255,0.5)', '&:hover': { color: 'white' } }}
|
||||
>
|
||||
<Github size={18} />
|
||||
</Link>
|
||||
|
||||
<Tooltip title="End-to-End Encrypted Local Session" variant="soft">
|
||||
<ShieldCheck size={18} className="text-slate-500 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<footer className="w-full bg-[#0A0A0A] border-t border-white/[0.05] px-6 py-3 flex items-center justify-between">
|
||||
<div className="flex gap-8">
|
||||
<StatMonitor
|
||||
icon={Cpu} label="CPU"
|
||||
value={`${stats.cpu}%`}
|
||||
percent={stats.cpu}
|
||||
color={stats.cpu > 80 ? "danger" : "primary"}
|
||||
/>
|
||||
<StatMonitor
|
||||
icon={Activity} label="GPU"
|
||||
value={`${stats.gpu.usage}%`}
|
||||
percent={stats.gpu.usage}
|
||||
color="success"
|
||||
/>
|
||||
<StatMonitor
|
||||
icon={Database} label="RAM"
|
||||
value={`${stats.ram_used.toFixed(1)}GB`}
|
||||
percent={(stats.ram_used / stats.ram_total) * 100}
|
||||
color="warning"
|
||||
/>
|
||||
<StatMonitor
|
||||
icon={HardDrive} label="DISK FREE"
|
||||
value={`${stats.disk_free_gb.toFixed(0)}GB`}
|
||||
percent={100}
|
||||
color="neutral"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Typography level="body-xs" sx={{ color: 'white/30', fontAlpha: 0.5 }}>
|
||||
ENG_MODE: ACTIVE | OS: {window.navigator.platform}
|
||||
</Typography>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
// Tooltip auxiliar (MUI Joy precisa do import)
|
||||
import { Tooltip } from "@mui/joy";
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Home, Settings, Cpu, Sparkles } from "lucide-react";
|
||||
|
||||
const appName = import.meta.env.VITE_APPNAME || "LocalMind AI";
|
||||
|
||||
export default function Header() {
|
||||
export default function Header({ onSettingsClick }: { onSettingsClick?: () => void }) {
|
||||
// Simulando um estado da IA (você conectaria isso ao seu backend real depois)
|
||||
const isModelLoaded = true;
|
||||
|
||||
@@ -85,6 +85,7 @@ export default function Header() {
|
||||
<IconButton
|
||||
variant="plain"
|
||||
color="neutral"
|
||||
onClick={onSettingsClick}
|
||||
sx={{
|
||||
color: '#94a3b8',
|
||||
'&:hover': { color: 'white', backgroundColor: 'rgba(255,255,255, 0.05)' }
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Modal, ModalDialog, ModalClose, Tabs, TabList, Tab, TabPanel, Sheet, List, ListItem, ListItemButton, ListItemContent, Button, Typography } from "@mui/joy";
|
||||
import { Download, CheckCircle2, Server } from "lucide-react";
|
||||
|
||||
export function SettingsModal({ open, onClose, models, onDownload }: any) {
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalDialog sx={{ bgcolor: '#0F0F0F', border: '1px solid rgba(255,255,255,0.1)', minWidth: 600 }}>
|
||||
<ModalClose sx={{ color: 'white' }} />
|
||||
<Typography level="h4" sx={{ color: 'white', mb: 2 }}>System Configuration</Typography>
|
||||
|
||||
<Tabs aria-label="Settings tabs" defaultValue={0} sx={{ bgcolor: 'transparent' }}>
|
||||
<TabList sx={{ bgcolor: 'rgba(255,255,255,0.03)', p: 0.5, borderRadius: 'lg' }}>
|
||||
<Tab disableIndicator sx={{ color: 'slate.400', '&.Mui-selected': { color: 'white', bgcolor: 'rgba(255,255,255,0.05)' } }}>
|
||||
Model Manager
|
||||
</Tab>
|
||||
<Tab disableIndicator sx={{ color: 'slate.400' }}>Inference Engine</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel value={0} sx={{ px: 0, py: 2 }}>
|
||||
<Sheet sx={{ bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 'md', maxHeight: 400, overflow: 'auto' }}>
|
||||
<List>
|
||||
{models.map((model: any) => (
|
||||
<ListItem key={model.id} sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<ListItemButton sx={{ gap: 2 }}>
|
||||
<Server className="text-cyan-500" />
|
||||
<ListItemContent>
|
||||
<Typography level="title-sm" sx={{ color: 'white' }}>{model.name} <span className="text-[10px] text-slate-500">{model.quant}</span></Typography>
|
||||
<Typography level="body-xs" sx={{ color: 'slate.400' }}>{model.size_gb} GB • {model.repo}</Typography>
|
||||
</ListItemContent>
|
||||
{model.downloaded ? (
|
||||
<Button size="sm" variant="soft" color="success" startDecorator={<CheckCircle2 size={16}/>}>Ready</Button>
|
||||
) : (
|
||||
<Button size="sm" variant="solid" color="primary" onClick={() => onDownload(model)} startDecorator={<Download size={16}/>}>Download</Button>
|
||||
)}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Sheet>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={1}>
|
||||
<Typography level="body-sm" sx={{ color: 'slate-400' }}>
|
||||
Hiperparâmetros de Engenharia (Threads, Context, GPU Offload) em desenvolvimento...
|
||||
</Typography>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user