feat: enhance UI with MUI and Tailwind integration
- Updated `package.json` to include new dependencies: Emotion, MUI Joy, Lucide React, and Tailwind CSS. - Refactored `src/App.tsx` to implement a new layout with a sidebar and chat interface using MUI components. - Added Tailwind CSS import in `src/App.css` for styling. - Improved user interaction elements with icons and tooltips for a better user experience.
This commit is contained in:
1 parent
ce33464c6e
commit
fb37807c31
17 files changed
+8087
-48
No files matched your search
Whitespace-only changes.
+16
-4
@@ -10,14 +10,26 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"preact": "^10.25.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource-variable/open-sans": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@mui/joy": "5.0.0-beta.52",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2"
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"dotenv": "^17.2.3",
|
||||
"lucide-react": "^0.562.0",
|
||||
"preact": "^10.25.1",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.9.3",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.3",
|
||||
"@tauri-apps/cli": "^2"
|
||||
"vite": "^6.0.3"
|
||||
}
|
||||
}
|
||||
Generated
+2393
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import os
|
||||
import yaml
|
||||
import psutil
|
||||
from abc import ABC, abstractmethod
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from huggingface_hub import hf_hub_download
|
||||
from llama_cpp import Llama
|
||||
|
||||
# --- Camada de Abstração (Interface) ---
|
||||
class AIModelEngine(ABC):
|
||||
@abstractmethod
|
||||
def generate_stream(self, prompt: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset_context(self):
|
||||
pass
|
||||
|
||||
# --- Gerenciador de Recursos de Engenharia ---
|
||||
class ResourceManager:
|
||||
@staticmethod
|
||||
def get_optimal_threads() -> int:
|
||||
# Prioriza núcleos físicos para evitar perdas por troca de contexto de hardware (SMT/Hyper-Threading)
|
||||
physical_cores = psutil.cpu_count(logical=False)
|
||||
return max(1, physical_cores - 1)
|
||||
|
||||
@staticmethod
|
||||
def get_available_ram_mb() -> int:
|
||||
return psutil.virtual_memory().available // (1024 * 1024)
|
||||
|
||||
# --- Implementação Llama-CPP ---
|
||||
class LlamaCPPEngine(AIModelEngine):
|
||||
def __init__(self, config: dict, model_path: str):
|
||||
threads = ResourceManager.get_optimal_threads()
|
||||
|
||||
self.llm = Llama(
|
||||
model_path=model_path,
|
||||
n_ctx=config.get('context_window', 2048),
|
||||
n_threads=threads,
|
||||
n_gpu_layers=0, # Auto-ajuste para estabilidade inicial
|
||||
n_batch=512,
|
||||
verbose=False
|
||||
)
|
||||
self.history = []
|
||||
self.config = config
|
||||
|
||||
def generate_stream(self, user_input: str):
|
||||
self.history.append({"role": "user", "content": user_input})
|
||||
|
||||
# Implementação de Sliding Window rudimentar (mantém últimos 5 turnos)
|
||||
if len(self.history) > 10:
|
||||
self.history = self.history[-10:]
|
||||
|
||||
try:
|
||||
stream = self.llm.create_chat_completion(
|
||||
messages=self.history,
|
||||
stream=True,
|
||||
temperature=self.config.get('temperature', 0.1),
|
||||
max_tokens=self.config.get('max_tokens', 512)
|
||||
)
|
||||
|
||||
full_response = ""
|
||||
for chunk in stream:
|
||||
delta = chunk['choices'][0].get('delta', {})
|
||||
if 'content' in delta:
|
||||
token = delta['content']
|
||||
full_response += token
|
||||
yield token
|
||||
|
||||
self.history.append({"role": "assistant", "content": full_response})
|
||||
|
||||
except Exception as e:
|
||||
self.reset_context()
|
||||
yield f"\n[ERRO CRÍTICO]: {str(e)}. Memória de contexto limpa."
|
||||
|
||||
def reset_context(self):
|
||||
self.history = []
|
||||
self.llm.reset()
|
||||
|
||||
# --- Engine Factory ---
|
||||
class EngineFactory:
|
||||
@staticmethod
|
||||
def create_engine(model_key: str, config_path: str, models_dir: str) -> AIModelEngine:
|
||||
with open(config_path, 'r') as f:
|
||||
config = yaml.safe_load(f)['models'][model_key]
|
||||
|
||||
model_path = hf_hub_download(
|
||||
repo_id=config['repo_id'],
|
||||
filename=config['filename'],
|
||||
local_dir=models_dir
|
||||
)
|
||||
|
||||
if config['type'] == 'llama-cpp':
|
||||
return LlamaCPPEngine(config, model_path)
|
||||
raise ValueError("Tipo de modelo não suportado.")
|
||||
|
||||
# --- Loop Principal com Threading ---
|
||||
def main():
|
||||
MODELS_DIR = "./src-python/app/models"
|
||||
CONFIG_FILE = "modelConfig.yaml"
|
||||
|
||||
print(f"[Sistema]: Detectando Hardware... Threads Otimizadas: {ResourceManager.get_optimal_threads()}")
|
||||
print(f"[Sistema]: RAM Disponível: {ResourceManager.get_available_ram_mb()} MB")
|
||||
|
||||
engine = EngineFactory.create_engine('lfm_2.5_1.2b', CONFIG_FILE, MODELS_DIR)
|
||||
|
||||
# Executor para gerenciar inferência fora da Main Thread
|
||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||
while True:
|
||||
try:
|
||||
user_in = input("\n[Você]: ").strip()
|
||||
if user_in.lower() in ['sair', 'exit']: break
|
||||
if user_in.lower() == 'clear':
|
||||
engine.reset_context()
|
||||
print("Contexto resetado.")
|
||||
continue
|
||||
if not user_in: continue
|
||||
|
||||
print("[IA]: ", end="", flush=True)
|
||||
|
||||
# Executa a geração em thread separada para não bloquear o poll de IO do sistema
|
||||
future = executor.submit(list, engine.generate_stream(user_in))
|
||||
|
||||
# Enquanto a thread processa, podemos exibir o stream
|
||||
# Nota: Para stream real em thread, usaríamos filas (Queue),
|
||||
# aqui simplificamos o consumo do gerador.
|
||||
for token in engine.generate_stream(user_in):
|
||||
print(token, end="", flush=True)
|
||||
print()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,14 @@
|
||||
models:
|
||||
lfm_2.5_1.2b:
|
||||
repo_id: "LiquidAI/LFM2.5-1.2B-Instruct-GGUF"
|
||||
filename: "LFM2.5-1.2B-Instruct-Q6_K.gguf"
|
||||
context_window: 2048
|
||||
temperature: 0.1
|
||||
max_tokens: 512
|
||||
type: "llama-cpp"
|
||||
|
||||
# Exemplo de futura expansão
|
||||
# phi3_mini:
|
||||
# repo_id: "microsoft/Phi-3-mini-4k-instruct-gguf"
|
||||
# filename: "Phi-3-mini-4k-instruct-q4.gguf"
|
||||
# type: "llama-cpp"
|
||||
@@ -0,0 +1,3 @@
|
||||
# requirements.txt gerado (estático, baseado na importação visível)
|
||||
llama-cpp-python>=0.2.0
|
||||
huggingface-hub>=0.20.0
|
||||
Generated
+5145
File diff suppressed because it is too large.
Load diff
@@ -1,3 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
|
||||
+181
-44
@@ -1,51 +1,188 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import preactLogo from "./assets/preact.svg";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import "./App.css";
|
||||
import "@fontsource/inter";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Sheet, Box, Textarea, IconButton, Tooltip, Typography,
|
||||
Menu, MenuItem, Dropdown, MenuButton, Badge
|
||||
} from "@mui/joy";
|
||||
import {
|
||||
Send, Plus, MessageSquare, Trash2, Command,
|
||||
Copy, RefreshCcw, ThumbsUp, ThumbsDown,
|
||||
Share2, Download, Search, Cpu, Paperclip, Mic, ChevronDown
|
||||
} from "lucide-react";
|
||||
|
||||
function App() {
|
||||
const [greetMsg, setGreetMsg] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
// Componentes de Interface Padronizados
|
||||
import Header from "./components/interface/Header";
|
||||
import Footer from "./components/interface/Footer";
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
setGreetMsg(await invoke("greet", { name }));
|
||||
}
|
||||
export default function App() {
|
||||
const [selectedModel, setSelectedModel] = useState("Llama-3.1-8B");
|
||||
|
||||
// Constantes de Design (Tokens)
|
||||
const ICON_SIZE = 18;
|
||||
const TOOLBAR_ICON_SIZE = 16;
|
||||
const BORDER_STYLE = "border-white/[0.06]";
|
||||
const SURFACE_COLOR = "bg-[#121212]";
|
||||
|
||||
return (
|
||||
<main class="container">
|
||||
<h1>Welcome to Tauri + Preact</h1>
|
||||
<div className="flex flex-col h-screen bg-[#0A0A0A] text-slate-200 overflow-hidden font-inter selection:bg-cyan-500/30">
|
||||
<Header />
|
||||
|
||||
<div class="row">
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src="/vite.svg" class="logo vite" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://tauri.app" target="_blank">
|
||||
<img src="/tauri.svg" class="logo tauri" alt="Tauri logo" />
|
||||
</a>
|
||||
<a href="https://preactjs.com" target="_blank">
|
||||
<img src={preactLogo} class="logo preact" alt="Preact logo" />
|
||||
</a>
|
||||
</div>
|
||||
<p>Click on the Tauri, Vite, and Preact logos to learn more.</p>
|
||||
<main className="flex flex-1 overflow-hidden border-t border-white/[0.05]">
|
||||
|
||||
{/* SIDEBAR - Design Homogêneo */}
|
||||
<aside className="hidden md:flex flex-col w-72 bg-[#0D0D0D] border-r border-white/[0.05]">
|
||||
<div className="p-4">
|
||||
<button className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl border border-white/[0.08] bg-white/[0.02] hover:bg-white/[0.06] transition-all text-sm font-semibold text-white">
|
||||
<Plus size={16} /> New Session
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-3 space-y-1">
|
||||
{['Otimização CUDA', 'Script Rust High-Level'].map((chat, i) => (
|
||||
<div key={i} className="group flex items-center justify-between p-2.5 rounded-xl hover:bg-white/[0.04] cursor-pointer transition-all">
|
||||
<div className="flex items-center gap-3 truncate">
|
||||
<MessageSquare size={16} className="text-slate-500" />
|
||||
<span className="text-sm truncate text-slate-400 group-hover:text-slate-200">{chat}</span>
|
||||
</div>
|
||||
<Trash2 size={14} className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 transition-all" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<form
|
||||
class="row"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
greet();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id="greet-input"
|
||||
onInput={(e) => setName(e.currentTarget.value)}
|
||||
placeholder="Enter a name..."
|
||||
/>
|
||||
<button type="submit">Greet</button>
|
||||
</form>
|
||||
<p>{greetMsg}</p>
|
||||
</main>
|
||||
{/* ÁREA DE CHAT */}
|
||||
<section className="flex-1 flex flex-col relative bg-[#0A0A0A]">
|
||||
<div className="flex-1 overflow-y-auto p-6 scrollbar-thin scrollbar-thumb-white/10">
|
||||
<div className="max-w-3xl mx-auto space-y-10">
|
||||
|
||||
{/* MENSAGEM DO USUÁRIO */}
|
||||
<div className="flex flex-col items-end animate-in slide-in-from-right-4 duration-300">
|
||||
<div className="bg-[#1A1A1A] border border-white/[0.08] px-5 py-3 rounded-[20px] rounded-tr-none max-w-[85%] shadow-lg shadow-black/20">
|
||||
<p className="text-[15px] leading-relaxed text-white">Quais as melhores estratégias de quantização atuais?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RESPOSTA DA IA (Baseada na Imagem 1) */}
|
||||
<div className="flex gap-4 group animate-in slide-in-from-left-4 duration-500">
|
||||
<div className="w-9 h-9 rounded-xl bg-cyan-500/10 border border-cyan-500/20 flex items-center justify-center shrink-0 shadow-inner">
|
||||
<Command size={20} className="text-cyan-400" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Typography level="body-xs" sx={{ color: 'rgba(255,255,255,0.25)', fontWeight: 800, letterSpacing: '0.05em' }}>
|
||||
ENGINE: {selectedModel.toUpperCase()}
|
||||
</Typography>
|
||||
<div className="text-[15px] leading-relaxed text-slate-300">
|
||||
Atualmente, as estratégias mais eficientes são:
|
||||
<ul className="list-disc ml-5 mt-2 space-y-1 text-slate-400">
|
||||
<li><strong>GGUF:</strong> Ideal para inferência local em CPU/GPU híbrida.</li>
|
||||
<li><strong>EXL2:</strong> Otimizada para GPUs NVIDIA com alta taxa de tokens/s.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BARRA DE INTERAÇÃO HOMOGÊNEA (IMAGE 1 STYLE) */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-white/[0.04]">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{[Share2, Download, Copy].map((Icon, idx) => (
|
||||
<Tooltip key={idx} title="Action" variant="soft">
|
||||
<IconButton size="sm" variant="plain" sx={{ color: '#64748b', '&:hover': { color: 'white', bgcolor: 'white/5' } }}>
|
||||
<Icon size={TOOLBAR_ICON_SIZE} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
{/* DROPDOWN REGENERATE */}
|
||||
<Dropdown>
|
||||
<Tooltip title="Regenerate with..." variant="soft">
|
||||
<MenuButton slots={{ root: IconButton }} slotProps={{ root: { size: 'sm', variant: 'plain', sx: { color: '#64748b', '&:hover': { color: '#06b6d4' } } } }}>
|
||||
<RefreshCcw size={TOOLBAR_ICON_SIZE} />
|
||||
<ChevronDown size={12} className="ml-0.5 opacity-50" />
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<Menu variant="outlined" sx={{ bgcolor: '#141414', borderColor: 'white/10', borderRadius: '12px', boxShadow: 'xl' }}>
|
||||
<MenuItem onClick={() => setSelectedModel("Llama-3.1-8B")}>Llama 3.1 8B</MenuItem>
|
||||
<MenuItem onClick={() => setSelectedModel("Mistral-Small")}>Mistral Small</MenuItem>
|
||||
<MenuItem onClick={() => setSelectedModel("Qwen-2.5-7B")}>Qwen 2.5 7B</MenuItem>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-0.5">
|
||||
<IconButton size="sm" variant="plain" sx={{ color: '#475569', '&:hover': { color: '#22c55e' } }}><ThumbsUp size={TOOLBAR_ICON_SIZE}/></IconButton>
|
||||
<IconButton size="sm" variant="plain" sx={{ color: '#475569', '&:hover': { color: '#ef4444' } }}><ThumbsDown size={TOOLBAR_ICON_SIZE}/></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ÁREA DE INPUT (Baseada na Imagem 2) */}
|
||||
<div className="p-6 bg-gradient-to-t from-[#0A0A0A] via-[#0A0A0A] to-transparent">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<Sheet
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderRadius: '24px',
|
||||
bgcolor: '#121212',
|
||||
borderColor: 'rgba(255,255,255,0.08)',
|
||||
p: 1.5,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
|
||||
'&:focus-within': { borderColor: 'rgba(6, 182, 212, 0.4)', bgcolor: '#141414' }
|
||||
}}
|
||||
>
|
||||
<Textarea
|
||||
placeholder="Ask a follow-up..."
|
||||
variant="plain"
|
||||
minRows={1}
|
||||
maxRows={8}
|
||||
sx={{ bgcolor: 'transparent', color: 'white', px: 2, fontSize: '15px' }}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', px: 1 }}>
|
||||
{/* Grupo de Busca/Ferramentas (Estilo Imagem 2) */}
|
||||
<Box sx={{ display: 'flex', gap: 0.5, bgcolor: 'rgba(255,255,255,0.03)', p: 0.5, borderRadius: '14px', border: '1px solid rgba(255,255,255,0.03)' }}>
|
||||
<IconButton size="sm" variant="plain" sx={{ color: '#06b6d4', bgcolor: 'rgba(6,182,212,0.12)', borderRadius: '10px' }}>
|
||||
<Search size={18}/>
|
||||
</IconButton>
|
||||
<IconButton size="sm" variant="plain" sx={{ color: '#64748b', '&:hover': { color: 'white' } }}>
|
||||
<Cpu size={18}/>
|
||||
</IconButton>
|
||||
<IconButton size="sm" variant="plain" sx={{ color: '#64748b', '&:hover': { color: 'white' } }}>
|
||||
<MessageSquare size={18}/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Grupo de Anexos e Envio */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton size="sm" variant="plain" sx={{ color: '#64748b' }}><Paperclip size={20}/></IconButton>
|
||||
<IconButton size="sm" variant="plain" sx={{ color: '#64748b' }}><Mic size={20}/></IconButton>
|
||||
<IconButton
|
||||
variant="solid"
|
||||
sx={{
|
||||
borderRadius: '12px',
|
||||
minHeight: '40px',
|
||||
minWidth: '40px',
|
||||
bgcolor: 'rgba(255,255,255,0.05)',
|
||||
color: 'rgba(255,255,255,0.2)',
|
||||
transition: 'all 0.3s',
|
||||
'&:hover': { bgcolor: '#06b6d4', color: 'white', transform: 'translateY(-1px)' }
|
||||
}}
|
||||
>
|
||||
<Send size={18} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import "@fontsource/inter";
|
||||
import { Typography, Divider, Link, Box } from "@mui/joy";
|
||||
import { Github, Database, Cpu, ShieldCheck, Terminal } 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
|
||||
|
||||
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>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
// Tooltip auxiliar (MUI Joy precisa do import)
|
||||
import { Tooltip } from "@mui/joy";
|
||||
@@ -0,0 +1,99 @@
|
||||
import "@fontsource/inter";
|
||||
import { Button, IconButton, Tooltip, Badge } from "@mui/joy";
|
||||
// Ícones são cruciais para um visual profissional.
|
||||
// Se não tiver, instale: npm install lucide-react
|
||||
import { Home, Settings, Cpu, Sparkles } from "lucide-react";
|
||||
|
||||
const appName = import.meta.env.VITE_APPNAME || "LocalMind AI";
|
||||
|
||||
export default function Header() {
|
||||
// Simulando um estado da IA (você conectaria isso ao seu backend real depois)
|
||||
const isModelLoaded = true;
|
||||
|
||||
return (
|
||||
<header
|
||||
className="
|
||||
sticky top-0 z-50
|
||||
flex items-center justify-between
|
||||
px-6 py-3
|
||||
/* Fundo escuro profundo com efeito de vidro fosco */
|
||||
bg-[#0A0A0A]/80 backdrop-blur-md
|
||||
/* Borda inferior sutil para definição */
|
||||
border-b border-white/[0.08]
|
||||
"
|
||||
>
|
||||
{/* --- ZONA DA MARCA --- */}
|
||||
<div className="flex items-center gap-3 group cursor-default">
|
||||
{/* Ícone da Marca com Gradiente Sutil */}
|
||||
<div className="relative flex items-center justify-center w-9 h-9 rounded-xl bg-gradient-to-br from-cyan-500/20 to-blue-600/20 border border-white/10 group-hover:border-cyan-500/50 transition-colors">
|
||||
<Sparkles size={20} className="text-cyan-400" />
|
||||
</div>
|
||||
|
||||
{/* Nome do App com Tipografia Inter Refinada */}
|
||||
<h1 className="font-inter font-bold text-xl tracking-tight text-white">
|
||||
{appName}
|
||||
{/* Um ponto sutil para indicar 'versão beta' ou 'local' */}
|
||||
<span className="text-cyan-500 text-xs ml-1 align-top">.local</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* --- ZONA DE AÇÕES E STATUS --- */}
|
||||
<nav className="flex items-center gap-1">
|
||||
{/* Indicador de Status da IA (Vital para apps locais) */}
|
||||
<div className="hidden md:flex items-center gap-2 px-3 py-1.5 mr-4 rounded-full bg-white/[0.03] border border-white/[0.05]">
|
||||
<Badge color={isModelLoaded ? "success" : "warning"} variant="solid" size="sm" anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
sx={{
|
||||
'& .MuiBadge-badge': {
|
||||
boxShadow: '0 0 0 2px #0A0A0A', // Borda escura para separar o badge
|
||||
minWidth: '8px', height: '8px', p: 0 // Badge minimalista
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Cpu size={16} className="text-slate-400" />
|
||||
</Badge>
|
||||
<span className="font-inter text-xs font-medium text-slate-300">
|
||||
{isModelLoaded ? "Model Ready" : "Loading Weights..."}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Navegação Principal - Estilo "Ghost Button" elegante */}
|
||||
<Button
|
||||
variant="plain"
|
||||
color="neutral"
|
||||
startDecorator={<Home size={18} />}
|
||||
sx={{
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
fontWeight: 600,
|
||||
color: '#94a3b8', // slate-400
|
||||
paddingX: '16px',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
color: 'white',
|
||||
backgroundColor: 'rgba(255,255,255, 0.05)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</Button>
|
||||
|
||||
{/* Separador vertical sutil */}
|
||||
<div className="h-6 w-[1px] bg-white/[0.1] mx-2"></div>
|
||||
|
||||
{/* Ícone de Configurações */}
|
||||
<Tooltip title="Settings" variant="soft" placement="bottom-end">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
color="neutral"
|
||||
sx={{
|
||||
color: '#94a3b8',
|
||||
'&:hover': { color: 'white', backgroundColor: 'rgba(255,255,255, 0.05)' }
|
||||
}}
|
||||
>
|
||||
<Settings size={20} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,4 +1,5 @@
|
||||
import { render } from "preact";
|
||||
import "./App.css";
|
||||
import App from "./App";
|
||||
|
||||
render(<App />, document.getElementById("root")!);
|
||||
@@ -0,0 +1,13 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
// Como você já usa o @fontsource/inter, vamos integrá-lo aqui
|
||||
inter: ["Inter", "sans-serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
Reference in new issue
Block a user