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:
Evandro Fonseca Junior committed 2026-01-14 23:57:14 -03:00
1 parent 482b28513e
commit 08e154894a
24 files changed
+1110 -91

No files matched your search

+86
View File
@@ -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,
)
+5
View File
@@ -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",
+186
View File
@@ -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]: {}
+37
View File
@@ -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);
}
+36
View File
@@ -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
}
+36
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
# Package initialization
+1
View File
@@ -0,0 +1 @@
# Package initialization
+139
View File
@@ -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)
+98
View File
@@ -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
+44
View File
@@ -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)
}
+7 -1
View File
@@ -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
+73
View File
@@ -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"
+1
View File
@@ -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"
+21
View File
@@ -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.
+11 -2
View File
@@ -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
}
]
}
]
}
+57
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
}
+52 -70
View File
@@ -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' }}>
&copy; {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";
}
+2 -1
View File
@@ -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>
);
}