1
This commit is contained in:
68
ai_agent.py
68
ai_agent.py
@@ -29,7 +29,7 @@ def get_llm_response(prompt: str, provider: str, cfg: dict) -> str:
|
|||||||
|
|
||||||
return "Provedor desconhecido."
|
return "Provedor desconhecido."
|
||||||
|
|
||||||
def query_agent(prompt: str, override_provider: str = None) -> str:
|
def query_agent(prompt: str, override_provider: str = None, chat_history: list = None) -> str:
|
||||||
"""
|
"""
|
||||||
Motor Agente em Loop (ReAct): Pensamento -> Ação -> Observação -> Resposta Final.
|
Motor Agente em Loop (ReAct): Pensamento -> Ação -> Observação -> Resposta Final.
|
||||||
"""
|
"""
|
||||||
@@ -39,41 +39,57 @@ def query_agent(prompt: str, override_provider: str = None) -> str:
|
|||||||
# Contexto de Ferramentas para a IA
|
# Contexto de Ferramentas para a IA
|
||||||
tools_desc = "\n".join([f"- {k}: {v['description']}" for k,v in AVAILABLE_TOOLS.items()])
|
tools_desc = "\n".join([f"- {k}: {v['description']}" for k,v in AVAILABLE_TOOLS.items()])
|
||||||
|
|
||||||
# Prompt especializado (sem chaves complexas)
|
# Prompt especializado reformulado para evitar alucinações
|
||||||
system_prompt_base = """Você é o [Antigravity VPS Agent].
|
system_prompt_base = """Você é o [Antigravity VPS Agent], o SysAdmin de elite do Marcos.
|
||||||
Sua missão é ser o SysAdmin de elite do Marcos. Você tem acesso root.
|
Você tem acesso root completo à VPS e deve agir de forma profissional e precisa.
|
||||||
|
|
||||||
### REGRAS:
|
### REGRAS DE OURO:
|
||||||
1. Responda em PORTUGUÊS (Brasil).
|
1. Responda em PORTUGUÊS (Brasil).
|
||||||
2. Para agir, use o formato: [TOOL:nome_da_ferramenta] argumento [/TOOL]. Rode UMA ferramenta por vez.
|
2. Se o usuário pedir o status da VPS, SEMPRE use a ferramenta 'get_system_health'.
|
||||||
3. Para comandos de terminal, use a ferramenta 'run_bash_command'. Exemplo: [TOOL:run_bash_command] ls -la [/TOOL].
|
3. Se o usuário pedir algo sobre containers, use 'get_docker_stats'.
|
||||||
4. Para ler arquivos do host, use 'read_vps_file'. Exemplo: [TOOL:read_vps_file] /etc/hosts [/TOOL].
|
4. Antes de decidir que um arquivo não existe, use 'run_bash_command' com 'ls' para verificar o diretório.
|
||||||
5. Após usar a ferramenta, você receberá a saída. O seu objetivo é resolver a solicitação do usuário.
|
5. NUCA invente que buscou por arquivos (como syslog.conf) se o usuário não pediu especificamente por eles.
|
||||||
6. Quando terminar, sua resposta FINAL deve ter:
|
6. A seção <REFINED> deve conter apenas as informações solicitadas. Se não houver imagem relevante, não inclua tags de imagem.
|
||||||
- Um resumo técnico rápido.
|
|
||||||
- Uma seção entre tags <REFINED> ... </REFINED> com uma tabela Markdown limpa ou resumo em tópicos (Nome: Valor) para o usuário leigo.
|
### FORMATO DE AÇÃO:
|
||||||
7. Se for solicitado exibir uma imagem (ex: um logo, foto) que você achou no host, **exiba-a dentro da tag <REFINED>** usando markdown de imagem com o endpoint `/api/host_file?path=`. Exemplo: ``
|
Use: [TOOL:nome_da_ferramenta] argumento [/TOOL]
|
||||||
|
Rode UMA ferramenta por vez. Aguarde a saída do SISTEMA antes de concluir.
|
||||||
|
|
||||||
|
### RESPOSTA FINAL:
|
||||||
|
Sua resposta terminada deve ter:
|
||||||
|
- Um resumo técnico.
|
||||||
|
- Uma seção <REFINED> ... </REFINED> com Markdown limpo.
|
||||||
|
- **DICA**: Só use imagens em <REFINED> se o usuário pediu para ver um arquivo de imagem específico que você localizou.
|
||||||
|
|
||||||
### FERRAMENTAS DISPONÍVEIS:
|
### FERRAMENTAS DISPONÍVEIS:
|
||||||
{TOOLS_LIST}
|
{TOOLS_LIST}
|
||||||
|
|
||||||
### EXEMPLO DE REFINAMENTO VISUAL:
|
### EXEMPLO DE SUCESSO:
|
||||||
Relatório: Coletei os dados solicitados.
|
Usuário: qual o uso de ram agora?
|
||||||
|
Agente: [TOOL:get_system_health] [/TOOL]
|
||||||
|
SISTEMA: CPU: 5% | RAM Usada: 20% | Disco Usado: 40%
|
||||||
|
Resposta: A memória RAM está operando com 20% de uso.
|
||||||
<REFINED>
|
<REFINED>
|
||||||
### 📊 Status Global
|
### 📊 Memória e CPU
|
||||||
- **CPU**: 10%
|
- **RAM Utilizada**: 20%
|
||||||
- **RAM**: 500MB livre
|
- **CPU**: 5%
|
||||||
|
|
||||||

|
|
||||||
</REFINED>
|
</REFINED>
|
||||||
"""
|
"""
|
||||||
system_prompt = system_prompt_base.replace("{TOOLS_LIST}", tools_desc)
|
system_prompt = system_prompt_base.replace("{TOOLS_LIST}", tools_desc)
|
||||||
|
|
||||||
history = f"\nUsuário: {prompt}\n"
|
# Constrói o histórico da conversa (memória de curto prazo)
|
||||||
max_loops = 10
|
history_str = ""
|
||||||
|
if chat_history:
|
||||||
|
for msg in chat_history[-5:]: # Pega as últimas 5 interações
|
||||||
|
history_str += f"\nUsuário: {msg['user']}\nAgente: {msg['bot']}\n"
|
||||||
|
|
||||||
|
history_str += f"\nUsuário: {prompt}\n"
|
||||||
|
|
||||||
|
current_iteration_history = history_str
|
||||||
|
max_loops = 8
|
||||||
|
|
||||||
for _ in range(max_loops):
|
for _ in range(max_loops):
|
||||||
full_prompt = system_prompt + history
|
full_prompt = system_prompt + current_iteration_history
|
||||||
response = get_llm_response(full_prompt, provider, cfg)
|
response = get_llm_response(full_prompt, provider, cfg)
|
||||||
|
|
||||||
# Procura por chamadas de ferramentas na resposta
|
# Procura por chamadas de ferramentas na resposta
|
||||||
@@ -91,10 +107,10 @@ Relatório: Coletei os dados solicitados.
|
|||||||
else:
|
else:
|
||||||
observation = AVAILABLE_TOOLS[tool_name]["func"](arg)
|
observation = AVAILABLE_TOOLS[tool_name]["func"](arg)
|
||||||
else:
|
else:
|
||||||
observation = f"Erro: Ferramenta '{tool_name}' não encontrada. Use as ferramentas listadas ou 'run_bash_command'."
|
observation = f"Erro: Ferramenta '{tool_name}' não encontrada."
|
||||||
|
|
||||||
# Adiciona ao histórico para a IA ler na próxima rodada
|
# Adiciona ao histórico do loop atual
|
||||||
history += f"\nAgente (Ação): {response}\nSISTEMA (Saída de {tool_name}): {observation}\n"
|
current_iteration_history += f"\nAgente (Ação): {response}\nSISTEMA (Saída de {tool_name}): {observation}\n"
|
||||||
else:
|
else:
|
||||||
# Se não tem comando, é a resposta final
|
# Se não tem comando, é a resposta final
|
||||||
return response
|
return response
|
||||||
|
|||||||
35
bot_logic.py
35
bot_logic.py
@@ -41,17 +41,28 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
if not await auth_check(update): return
|
if not await auth_check(update): return
|
||||||
await update.message.reply_text("👋 Olá, Marcos! Antigravity VPS Agent online e pronto para receber comandos.")
|
await update.message.reply_text("👋 Olá, Marcos! Antigravity VPS Agent online e pronto para receber comandos.")
|
||||||
|
|
||||||
|
# Memória persistente da conversa (em memória RAM)
|
||||||
|
chat_histories = {}
|
||||||
|
|
||||||
async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
if not await auth_check(update): return
|
if not await auth_check(update): return
|
||||||
|
|
||||||
|
chat_id = update.message.chat_id
|
||||||
user_msg = update.message.text
|
user_msg = update.message.text
|
||||||
await update.message.reply_chat_action(action="typing")
|
await update.message.reply_chat_action(action="typing")
|
||||||
|
|
||||||
|
# Busca histórico anterior
|
||||||
|
history = chat_histories.get(chat_id, [])
|
||||||
|
|
||||||
# Aciona o Agente de IA para processar o prompt e executar Tools se precisar
|
# Aciona o Agente de IA para processar o prompt e executar Tools se precisar
|
||||||
from config import get_config
|
from config import get_config
|
||||||
cfg = get_config()
|
cfg = get_config()
|
||||||
reply = query_agent(prompt=user_msg, override_provider=cfg.get("active_provider"))
|
reply = query_agent(prompt=user_msg, override_provider=cfg.get("active_provider"), chat_history=history)
|
||||||
|
|
||||||
|
# Atualiza histórico
|
||||||
|
history.append({"user": user_msg, "bot": reply})
|
||||||
|
chat_histories[chat_id] = history[-10:] # Mantém apenas as últimas 10
|
||||||
|
|
||||||
# Se o usuário pedir ativamente por áudio no texto
|
# Se o usuário pedir ativamente por áudio no texto
|
||||||
if "áudio" in user_msg.lower() or "audio" in user_msg.lower() or "voz" in user_msg.lower():
|
if "áudio" in user_msg.lower() or "audio" in user_msg.lower() or "voz" in user_msg.lower():
|
||||||
await update.message.reply_chat_action(action="record_voice")
|
await update.message.reply_chat_action(action="record_voice")
|
||||||
@@ -59,8 +70,6 @@ async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
if audio_path:
|
if audio_path:
|
||||||
await update.message.reply_voice(voice=open(audio_path, 'rb'))
|
await update.message.reply_voice(voice=open(audio_path, 'rb'))
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
reply += "\n\n*(Falha ao gerar áudio com a ElevenLabs. Serviço indisponível.)*"
|
|
||||||
|
|
||||||
# Responde no chat normalmente
|
# Responde no chat normalmente
|
||||||
await update.message.reply_text(reply)
|
await update.message.reply_text(reply)
|
||||||
@@ -93,10 +102,18 @@ async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
text = recognizer.recognize_google(audio_data, language="pt-BR")
|
text = recognizer.recognize_google(audio_data, language="pt-BR")
|
||||||
await update.message.reply_text(f"🗣️ Reconhecido: _{text}_", parse_mode="Markdown")
|
await update.message.reply_text(f"🗣️ Reconhecido: _{text}_", parse_mode="Markdown")
|
||||||
|
|
||||||
|
# Busca histórico anterior
|
||||||
|
chat_id = update.message.chat_id
|
||||||
|
history = chat_histories.get(chat_id, [])
|
||||||
|
|
||||||
# Envia o texto reconhecido para o Agente (respeitando a configuração ativa)
|
# Envia o texto reconhecido para o Agente (respeitando a configuração ativa)
|
||||||
from config import get_config
|
from config import get_config
|
||||||
cfg = get_config()
|
cfg = get_config()
|
||||||
reply = query_agent(prompt=text, override_provider=cfg.get("active_provider"))
|
reply = query_agent(prompt=text, override_provider=cfg.get("active_provider"), chat_history=history)
|
||||||
|
|
||||||
|
# Atualiza histórico
|
||||||
|
history.append({"user": text, "bot": reply})
|
||||||
|
chat_histories[chat_id] = history[-10:]
|
||||||
|
|
||||||
# Sintetiza com ElevenLabs e responde com Áudio
|
# Sintetiza com ElevenLabs e responde com Áudio
|
||||||
audio_path = synthesize_audio(reply)
|
audio_path = synthesize_audio(reply)
|
||||||
@@ -129,12 +146,22 @@ async def llm_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
else:
|
else:
|
||||||
await update.message.reply_text("Modelos disponíveis: gemini ou ollama.")
|
await update.message.reply_text("Modelos disponíveis: gemini ou ollama.")
|
||||||
|
|
||||||
|
async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not await auth_check(update): return
|
||||||
|
chat_id = update.message.chat_id
|
||||||
|
if chat_id in chat_histories:
|
||||||
|
chat_histories[chat_id] = []
|
||||||
|
await update.message.reply_text("🧹 Memória limpa com sucesso!")
|
||||||
|
else:
|
||||||
|
await update.message.reply_text("A memória já está vazia.")
|
||||||
|
|
||||||
def get_telegram_app():
|
def get_telegram_app():
|
||||||
if not TOKEN:
|
if not TOKEN:
|
||||||
raise ValueError("TELEGRAM_BOT_TOKEN não encontrado no .env")
|
raise ValueError("TELEGRAM_BOT_TOKEN não encontrado no .env")
|
||||||
app = Application.builder().token(TOKEN).build()
|
app = Application.builder().token(TOKEN).build()
|
||||||
app.add_handler(CommandHandler("start", start))
|
app.add_handler(CommandHandler("start", start))
|
||||||
app.add_handler(CommandHandler("llm", llm_command))
|
app.add_handler(CommandHandler("llm", llm_command))
|
||||||
|
app.add_handler(CommandHandler("limpar", clear_history))
|
||||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
|
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
|
||||||
app.add_handler(MessageHandler(filters.VOICE, handle_voice))
|
app.add_handler(MessageHandler(filters.VOICE, handle_voice))
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="pt-BR" data-theme="dark">
|
<html lang="pt-BR" data-theme="dark">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>VPS AI Dashboard</title>
|
<title>VPS AI Dashboard</title>
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>">
|
<link rel="icon"
|
||||||
|
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%F0%9F%A4%96%3C/text%3E%3C/svg%3E">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
@@ -27,36 +29,50 @@
|
|||||||
--transition: 0.25s ease;
|
--transition: 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar */
|
|
||||||
::-webkit-scrollbar { width: 8px; }
|
|
||||||
::-webkit-scrollbar-track { background: var(--bg-base); }
|
|
||||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; border: 2px solid var(--bg-base); }
|
|
||||||
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
|
|
||||||
|
|
||||||
/* Scrollbar Fix for Firefox */
|
|
||||||
* { scrollbar-width: thin; scrollbar-color: var(--accent) transparent; }
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% { transform: scale(1); opacity: 1; }
|
|
||||||
50% { transform: scale(1.1); opacity: 0.7; }
|
|
||||||
100% { transform: scale(1); opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Scrollbar */
|
/* Custom Scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: var(--bg-base);
|
background: var(--bg-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
border: 2px solid var(--bg-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Fix for Firefox */
|
||||||
|
@supports (scrollbar-width: thin) {
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--accent) transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[data-theme="light"] {
|
[data-theme="light"] {
|
||||||
--bg-base: #f1f5f9;
|
--bg-base: #f1f5f9;
|
||||||
--bg-card: #ffffff;
|
--bg-card: #ffffff;
|
||||||
@@ -70,6 +86,15 @@
|
|||||||
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Global SVG Presentation */
|
||||||
|
svg {
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -396,21 +421,34 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.insights-content h3 { margin-bottom: 1rem; color: var(--accent); }
|
.insights-content h3 {
|
||||||
.insights-content table {
|
margin-bottom: 1rem;
|
||||||
width: 100%;
|
color: var(--accent);
|
||||||
border-collapse: collapse;
|
}
|
||||||
margin: 1rem 0;
|
|
||||||
|
.insights-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1rem 0;
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.insights-content th, .insights-content td {
|
|
||||||
padding: 0.75rem;
|
.insights-content th,
|
||||||
text-align: left;
|
.insights-content td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.insights-content th { background: rgba(59, 130, 246, 0.1); color: var(--accent); font-size: 0.75rem; text-transform: uppercase; }
|
|
||||||
|
.insights-content th {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.insights-placeholder {
|
.insights-placeholder {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -567,6 +605,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
transition: opacity 0.5s ease, visibility 0.5s;
|
transition: opacity 0.5s ease, visibility 0.5s;
|
||||||
}
|
}
|
||||||
@@ -592,98 +631,180 @@
|
|||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
letter-spacing: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-error {
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-icon-sun {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-icon-moon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .theme-icon-sun {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .theme-icon-moon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area {
|
||||||
|
padding: 0.75rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recording-indicator {
|
||||||
|
display: none;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: red;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-placeholder small {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="login-overlay">
|
<div id="login-overlay">
|
||||||
<div class="card login-card">
|
<div class="card login-card">
|
||||||
<div style="font-size: 3rem; margin-bottom: 1rem;">🔒</div>
|
<div class="login-icon" role="img" aria-label="Cadeado de Segurança">🔒</div>
|
||||||
<h2>Acesso Restrito</h2>
|
<h2>Acesso Restrito</h2>
|
||||||
<p style="color:var(--text-muted); margin-bottom:1.5rem; font-size:0.85rem;">Esta VPS está protegida. Insira a senha mestra para gerenciar o Agente.</p>
|
<p class="login-desc">Esta VPS está protegida. Insira a senha mestra para gerenciar o Agente.</p>
|
||||||
<input type="password" id="web-pass-input" class="form-input" placeholder="Senha da VPS" style="margin-bottom:1rem; text-align:center; font-size: 1.1rem; letter-spacing: 0.2rem;" onkeypress="if(event.key==='Enter') attemptLogin()">
|
<input type="password" id="web-pass-input" aria-label="Senha da VPS" autocomplete="current-password"
|
||||||
<button class="btn btn-primary" style="width:100%; padding: 0.8rem;" onclick="attemptLogin()">Entrar no Dashboard</button>
|
class="form-input login-input" placeholder="Senha da VPS"
|
||||||
<div id="login-error" style="color:var(--danger); font-size: 0.75rem; margin-top: 1rem; display: none;">Senha incorreta. Tente novamente.</div>
|
onkeypress="if(event.key==='Enter') attemptLogin()">
|
||||||
|
<button type="button" class="btn btn-primary login-btn"
|
||||||
|
onclick="attemptLogin()">Entrar no Dashboard</button>
|
||||||
|
<div id="login-error">Senha incorreta. Tente novamente.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
||||||
</svg>
|
</svg>
|
||||||
<h1>VPS AI Dashboard</h1>
|
<h1>VPS AI Dashboard</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="status-badge" id="bot-status">Online</div>
|
<div class="status-badge" id="bot-status">Online</div>
|
||||||
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Alternar tema">
|
<button type="button" class="theme-toggle" onclick="toggleTheme()" aria-label="Alternar tema">
|
||||||
<svg id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none;">
|
<svg id="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="theme-icon-sun">
|
||||||
<circle cx="12" cy="12" r="5"/>
|
<circle cx="12" cy="12" r="5" />
|
||||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
<path
|
||||||
|
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg id="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="theme-icon-moon">
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="card stat-card">
|
<div class="card stat-card">
|
||||||
<h3>CPU</h3>
|
<h3>CPU</h3>
|
||||||
<div class="stat-value" id="cpu-val">--%</div>
|
<div class="stat-value" id="cpu-val">--%</div>
|
||||||
<div class="progress-bar-bg">
|
<div class="progress-bar-bg">
|
||||||
<div class="progress-bar" id="cpu-bar" style="width: 0%"></div>
|
<div class="progress-bar" id="cpu-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card stat-card">
|
<div class="card stat-card">
|
||||||
<h3>RAM</h3>
|
<h3>RAM</h3>
|
||||||
<div class="stat-value" id="ram-val">-- / -- GB</div>
|
<div class="stat-value" id="ram-val">-- / -- GB</div>
|
||||||
<div class="progress-bar-bg">
|
<div class="progress-bar-bg">
|
||||||
<div class="progress-bar" id="ram-bar" style="width: 0%"></div>
|
<div class="progress-bar" id="ram-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card stat-card">
|
<div class="card stat-card">
|
||||||
<h3>Disk</h3>
|
<h3>Disk</h3>
|
||||||
<div class="stat-value" id="disk-val">--%</div>
|
<div class="stat-value" id="disk-val">--%</div>
|
||||||
<div class="progress-bar-bg">
|
<div class="progress-bar-bg">
|
||||||
<div class="progress-bar" id="disk-bar" style="width: 0%"></div>
|
<div class="progress-bar" id="disk-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-title">Ações Rápidas</div>
|
<div class="section-title">Ações Rápidas</div>
|
||||||
<div class="actions-grid">
|
<div class="actions-grid">
|
||||||
<button class="btn" onclick="executeAction('ping')">
|
<button type="button" class="btn" aria-label="Ping" onclick="executeAction('ping')">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||||
</svg>
|
</svg>
|
||||||
Ping
|
Ping
|
||||||
</button>
|
</button>
|
||||||
<button class="btn" id="btn-test-llm" onclick="testLLMSpeed()">
|
<button type="button" class="btn" id="btn-test-llm" aria-label="Testar LLM" onclick="testLLMSpeed()">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M12 2a10 10 0 1 0 10 10H12V2z"/>
|
<path d="M12 2a10 10 0 1 0 10 10H12V2z" />
|
||||||
<path d="M12 2a10 10 0 0 1 10 10h-2a8 8 0 0 0-8-8V2z"/>
|
<path d="M12 2a10 10 0 0 1 10 10h-2a8 8 0 0 0-8-8V2z" />
|
||||||
<path d="M12 12V2.5l5.5 5.5"/>
|
<path d="M12 12V2.5l5.5 5.5" />
|
||||||
</svg>
|
</svg>
|
||||||
Testar LLM
|
Testar LLM
|
||||||
</button>
|
</button>
|
||||||
<button class="btn" onclick="executeAction('restart_bot')">
|
<button type="button" class="btn" aria-label="Reiniciar Bot" onclick="executeAction('restart_bot')">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M23 4v6h-6M1 20v-6h6"/>
|
<path d="M23 4v6h-6M1 20v-6h6" />
|
||||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||||
</svg>
|
</svg>
|
||||||
Reiniciar
|
Reiniciar
|
||||||
</button>
|
</button>
|
||||||
<button class="btn" onclick="executeAction('clear_cache')">
|
<button type="button" class="btn" aria-label="Limpar Cache" onclick="executeAction('clear_cache')">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||||
</svg>
|
</svg>
|
||||||
Limpar Cache
|
Limpar Cache
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" onclick="executeAction('reboot_vps')">
|
<button type="button" class="btn btn-danger" aria-label="Reboot VPS" onclick="executeAction('reboot_vps')">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||||
</svg>
|
</svg>
|
||||||
Reboot VPS
|
Reboot VPS
|
||||||
</button>
|
</button>
|
||||||
@@ -693,22 +814,23 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="config-grid">
|
<div class="config-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Provider Ativo</label>
|
<label for="active_provider">Provider Ativo</label>
|
||||||
<select id="active_provider" class="form-input">
|
<select id="active_provider" aria-label="Provider Ativo" class="form-input">
|
||||||
<option value="ollama">Ollama (Local)</option>
|
<option value="ollama">Ollama (Local)</option>
|
||||||
<option value="gemini">Gemini Pro (Google)</option>
|
<option value="gemini">Gemini Pro (Google)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Gemini API Key</label>
|
<label for="gemini_api_key">Gemini API Key</label>
|
||||||
<input type="password" id="gemini_api_key" class="form-input" placeholder="AIzaSy...">
|
<input type="password" id="gemini_api_key" autocomplete="new-password" aria-label="Gemini API Key"
|
||||||
|
class="form-input" placeholder="AIzaSy...">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="saveConfiguration()">
|
<button type="button" class="btn btn-primary" aria-label="Salvar Configuração" onclick="saveConfiguration()">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
|
||||||
<polyline points="17 21 17 13 7 13 7 21"/>
|
<polyline points="17 21 17 13 7 13 7 21" />
|
||||||
<polyline points="7 3 7 8 15 8"/>
|
<polyline points="7 3 7 8 15 8" />
|
||||||
</svg>
|
</svg>
|
||||||
Salvar
|
Salvar
|
||||||
</button>
|
</button>
|
||||||
@@ -720,24 +842,27 @@
|
|||||||
<div class="chat-wrapper">
|
<div class="chat-wrapper">
|
||||||
<div class="chat-messages" id="chat-box">
|
<div class="chat-messages" id="chat-box">
|
||||||
<div class="chat-bubble bubble-ai">
|
<div class="chat-bubble bubble-ai">
|
||||||
Olá! Sou o VPS Agent. Como posso ajudar com seu servidor? Tudo o que eu fizer aparecerá aqui no terminal técnico.
|
Olá! Sou o VPS Agent. Como posso ajudar com seu servidor? Tudo o que eu fizer aparecerá aqui no
|
||||||
|
terminal técnico.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-input-area" style="padding: 0.75rem; gap: 0.5rem;">
|
<div class="chat-input-area">
|
||||||
<input type="text" id="chat-input" class="chat-input" placeholder="Comande sua VPS aqui..." onkeypress="handleKeyPress(event)">
|
<input type="text" id="chat-input" aria-label="Comando do chat" class="chat-input"
|
||||||
|
placeholder="Comande sua VPS aqui..." onkeypress="handleKeyPress(event)">
|
||||||
<button class="btn" id="audio-btn" onclick="toggleRecording()" title="Gravar Áudio">
|
|
||||||
<svg id="mic-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<button type="button" class="btn" id="audio-btn" aria-label="Gravar Áudio"
|
||||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
onclick="toggleRecording()" title="Gravar Áudio">
|
||||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8"/>
|
<svg id="mic-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
|
||||||
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8" />
|
||||||
</svg>
|
</svg>
|
||||||
<div id="recording-dot" style="display:none; width:8px; height:8px; background:red; border-radius:50%; animation: pulse 1s infinite;"></div>
|
<span id="recording-dot" class="recording-indicator"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-primary" onclick="sendChat()">
|
<button type="button" class="btn btn-primary" aria-label="Enviar mensagem" onclick="sendChat()">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<line x1="22" y1="2" x2="11" y2="13"/>
|
<line x1="22" y1="2" x2="11" y2="13" />
|
||||||
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -746,22 +871,22 @@
|
|||||||
<!-- Coluna 2: Painel de Insights (Refinado) -->
|
<!-- Coluna 2: Painel de Insights (Refinado) -->
|
||||||
<div class="insights-wrapper">
|
<div class="insights-wrapper">
|
||||||
<div class="insights-header">
|
<div class="insights-header">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||||
<path d="M21 12V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7"/>
|
<path d="M21 12V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7" />
|
||||||
<line x1="16" y1="5" x2="16" y2="19"/>
|
<line x1="16" y1="5" x2="16" y2="19" />
|
||||||
<line x1="2" x2="16" y2="12"/>
|
<line x1="2" x2="16" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
Painel de Insights Visuais
|
Painel de Insights Visuais
|
||||||
</div>
|
</div>
|
||||||
<div class="insights-content" id="insights-panel">
|
<div class="insights-content" id="insights-panel">
|
||||||
<div class="insights-placeholder">
|
<div class="insights-placeholder">
|
||||||
<div style="font-size: 2.5rem; margin-bottom: 1rem;">📊</div>
|
<div class="insights-icon" role="img" aria-label="Gráfico de Insights">📊</div>
|
||||||
<p>Aguardando dados estruturados...</p>
|
<p>Aguardando dados estruturados...</p>
|
||||||
<small style="display:block; margin-top:0.5rem; opacity:0.6;">Peça algo como "status dos containers" para ver o refinamento aqui.</small>
|
<small>Peça algo como "status dos containers" para ver o refinamento aqui.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toast">Ação executada!</div>
|
<div id="toast">Ação executada!</div>
|
||||||
@@ -773,7 +898,7 @@
|
|||||||
async function apiFetch(url, options = {}) {
|
async function apiFetch(url, options = {}) {
|
||||||
if (!options.headers) options.headers = {};
|
if (!options.headers) options.headers = {};
|
||||||
options.headers['X-Web-Password'] = webPassword;
|
options.headers['X-Web-Password'] = webPassword;
|
||||||
|
|
||||||
const res = await fetch(url, options);
|
const res = await fetch(url, options);
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
showLoginOverlay();
|
showLoginOverlay();
|
||||||
@@ -822,15 +947,11 @@
|
|||||||
updateThemeIcon(!isDark);
|
updateThemeIcon(!isDark);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateThemeIcon(isDark) {
|
function updateThemeIcon(isDark) {}
|
||||||
document.getElementById('icon-sun').style.display = isDark ? 'block' : 'none';
|
|
||||||
document.getElementById('icon-moon').style.display = isDark ? 'none' : 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
if (savedTheme) {
|
if (savedTheme) {
|
||||||
document.documentElement.dataset.theme = savedTheme;
|
document.documentElement.dataset.theme = savedTheme;
|
||||||
updateThemeIcon(savedTheme === 'light');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
@@ -874,7 +995,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(() => { if(!document.getElementById('login-overlay').classList.contains('hidden')) return; fetchStats(); }, 3000);
|
setInterval(() => { if (!document.getElementById('login-overlay').classList.contains('hidden')) return; fetchStats(); }, 3000);
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
async function executeAction(type) {
|
async function executeAction(type) {
|
||||||
@@ -901,7 +1022,7 @@
|
|||||||
|
|
||||||
function showToast(msg, isError = false) {
|
function showToast(msg, isError = false) {
|
||||||
const toast = document.getElementById('toast');
|
const toast = document.getElementById('toast');
|
||||||
if(!toast) return;
|
if (!toast) return;
|
||||||
toast.textContent = msg;
|
toast.textContent = msg;
|
||||||
toast.className = isError ? 'error show' : 'show';
|
toast.className = isError ? 'error show' : 'show';
|
||||||
setTimeout(() => toast.classList.remove('show'), 3000);
|
setTimeout(() => toast.classList.remove('show'), 3000);
|
||||||
@@ -931,7 +1052,7 @@
|
|||||||
function processAIReply(fullText) {
|
function processAIReply(fullText) {
|
||||||
// Separa a parte técnica da parte refinada
|
// Separa a parte técnica da parte refinada
|
||||||
const refinedMatch = fullText.match(/<REFINED>([\s\S]*?)<\/REFINED>/i);
|
const refinedMatch = fullText.match(/<REFINED>([\s\S]*?)<\/REFINED>/i);
|
||||||
|
|
||||||
let technicalPart = fullText;
|
let technicalPart = fullText;
|
||||||
if (refinedMatch) {
|
if (refinedMatch) {
|
||||||
technicalPart = fullText.replace(refinedMatch[0], '').trim();
|
technicalPart = fullText.replace(refinedMatch[0], '').trim();
|
||||||
@@ -946,11 +1067,11 @@
|
|||||||
|
|
||||||
function updateInsightsPanel(markdown) {
|
function updateInsightsPanel(markdown) {
|
||||||
const panel = document.getElementById('insights-panel');
|
const panel = document.getElementById('insights-panel');
|
||||||
if(!panel) return;
|
if (!panel) return;
|
||||||
|
|
||||||
// Injeta a senha web nas URLs das imagens para autenticação inline
|
// Injeta a senha web nas URLs das imagens para autenticação inline
|
||||||
const mdWithAuth = markdown.replace(/\/api\/host_file\?path=/g, '/api/host_file?pwd=' + encodeURIComponent(webPassword) + '&path=');
|
const mdWithAuth = markdown.replace(/\/api\/host_file\?path=/g, '/api/host_file?pwd=' + encodeURIComponent(webPassword) + '&path=');
|
||||||
|
|
||||||
// Renderiza o Markdown para HTML usando marked.js
|
// Renderiza o Markdown para HTML usando marked.js
|
||||||
panel.innerHTML = `<div class="animate-fade-in">${marked.parse(mdWithAuth)}</div>`;
|
panel.innerHTML = `<div class="animate-fade-in">${marked.parse(mdWithAuth)}</div>`;
|
||||||
}
|
}
|
||||||
@@ -961,7 +1082,7 @@
|
|||||||
|
|
||||||
function addBubble(text, sender) {
|
function addBubble(text, sender) {
|
||||||
const box = document.getElementById('chat-box');
|
const box = document.getElementById('chat-box');
|
||||||
if(!box) return;
|
if (!box) return;
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'chat-bubble bubble-' + sender;
|
div.className = 'chat-bubble bubble-' + sender;
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
@@ -977,7 +1098,7 @@
|
|||||||
const key = document.getElementById('gemini_api_key');
|
const key = document.getElementById('gemini_api_key');
|
||||||
if (provider) provider.value = data.active_provider || 'ollama';
|
if (provider) provider.value = data.active_provider || 'ollama';
|
||||||
if (key) key.value = data.gemini_api_key || '';
|
if (key) key.value = data.gemini_api_key || '';
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
@@ -1010,20 +1131,20 @@
|
|||||||
try {
|
try {
|
||||||
const res = await apiFetch('/api/test_llm');
|
const res = await apiFetch('/api/test_llm');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if(data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
showToast(`✅ LLM Online! Resposta em ${data.latency}s`);
|
showToast(`✅ LLM Online! Resposta em ${data.latency}s`);
|
||||||
btn.innerHTML = `✅ ${data.latency}s`;
|
btn.innerHTML = `✅ ${data.latency}s`;
|
||||||
setTimeout(() => { btn.innerHTML = originalContent; btn.disabled = false; }, 5000);
|
setTimeout(() => { btn.innerHTML = originalContent; btn.disabled = false; }, 5000);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.message);
|
throw new Error(data.message);
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
showToast("❌ Erro no Teste LLM: " + e.message, true);
|
showToast("❌ Erro no Teste LLM: " + e.message, true);
|
||||||
btn.innerHTML = '❌ Falhou';
|
btn.innerHTML = '❌ Falhou';
|
||||||
btn.classList.add('btn-danger');
|
btn.classList.add('btn-danger');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.innerHTML = originalContent;
|
btn.innerHTML = originalContent;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.classList.remove('btn-danger');
|
btn.classList.remove('btn-danger');
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
@@ -1074,31 +1195,31 @@
|
|||||||
const dot = document.getElementById('recording-dot');
|
const dot = document.getElementById('recording-dot');
|
||||||
const icon = document.getElementById('mic-icon');
|
const icon = document.getElementById('mic-icon');
|
||||||
const btn = document.getElementById('audio-btn');
|
const btn = document.getElementById('audio-btn');
|
||||||
|
|
||||||
dot.style.display = active ? 'block' : 'none';
|
if (dot) dot.style.display = active ? 'inline-block' : 'none';
|
||||||
icon.style.color = active ? 'red' : 'inherit';
|
if (icon) icon.style.color = active ? 'red' : 'inherit';
|
||||||
btn.style.borderColor = active ? 'red' : 'var(--border)';
|
if (btn) btn.style.borderColor = active ? 'red' : 'var(--border)';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadAudio(blob) {
|
async function uploadAudio(blob) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('audio', blob, 'voice.webm');
|
formData.append('audio', blob, 'voice.webm');
|
||||||
|
|
||||||
showToast("✨ Transcrevendo áudio...");
|
showToast("✨ Transcrevendo áudio...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch('/api/chat-audio', {
|
const res = await apiFetch('/api/chat-audio', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if(data.text) {
|
if (data.text) {
|
||||||
addBubble(data.text, 'user');
|
addBubble(data.text, 'user');
|
||||||
processAIReply(data.reply);
|
processAIReply(data.reply);
|
||||||
|
|
||||||
// Se o bot retornou áudio, toca ele
|
// Se o bot retornou áudio, toca ele
|
||||||
if(data.audio_url) {
|
if (data.audio_url) {
|
||||||
const audio = new Audio(data.audio_url + '?t=' + Date.now());
|
const audio = new Audio(data.audio_url + '?t=' + Date.now());
|
||||||
audio.play();
|
audio.play();
|
||||||
}
|
}
|
||||||
@@ -1121,11 +1242,12 @@
|
|||||||
} else {
|
} else {
|
||||||
showLoginOverlay();
|
showLoginOverlay();
|
||||||
}
|
}
|
||||||
} catch(e) { showLoginOverlay(); }
|
} catch (e) { showLoginOverlay(); }
|
||||||
})();
|
})();
|
||||||
} else {
|
} else {
|
||||||
showLoginOverlay();
|
showLoginOverlay();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
17
tools.py
17
tools.py
@@ -1,6 +1,7 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
|
import time
|
||||||
|
|
||||||
def run_bash_command(command: str) -> str:
|
def run_bash_command(command: str) -> str:
|
||||||
"""Executa um comando bash na VPS e retorna a saída."""
|
"""Executa um comando bash na VPS e retorna a saída."""
|
||||||
@@ -26,11 +27,19 @@ def run_bash_command(command: str) -> str:
|
|||||||
return f"ERRO fatal ao rodar bash: {str(e)}"
|
return f"ERRO fatal ao rodar bash: {str(e)}"
|
||||||
|
|
||||||
def get_system_health() -> str:
|
def get_system_health() -> str:
|
||||||
"""Retorna um texto base rápido da saúde atual do servidor para a IA analisar."""
|
"""Retorna um texto detalhado da saúde do servidor para a IA."""
|
||||||
cpu = psutil.cpu_percent(interval=0.1)
|
cpu = psutil.cpu_percent(interval=0.1)
|
||||||
ram = psutil.virtual_memory().percent
|
vm = psutil.virtual_memory()
|
||||||
disk = psutil.disk_usage('/').percent
|
disk = psutil.disk_usage('/')
|
||||||
return f"CPU: {cpu}% | RAM Usada: {ram}% | Disco Usado: {disk}%"
|
|
||||||
|
# Uptime aproximado usando psutil
|
||||||
|
uptime_seconds = time.time() - psutil.boot_time()
|
||||||
|
uptime_hours = round(uptime_seconds / 3600, 1)
|
||||||
|
|
||||||
|
return (f"CPU: {cpu}% | "
|
||||||
|
f"RAM: {round(vm.used / (1024**3), 2)}GB usada / {round(vm.total / (1024**3), 2)}GB total ({vm.percent}%) | "
|
||||||
|
f"Disco: {disk.percent}% usado | "
|
||||||
|
f"Uptime: {uptime_hours}h")
|
||||||
|
|
||||||
def read_vps_file(filepath: str) -> str:
|
def read_vps_file(filepath: str) -> str:
|
||||||
"""Lê um arquivo do sistema de arquivos da VPS através do mapeamento /host_root."""
|
"""Lê um arquivo do sistema de arquivos da VPS através do mapeamento /host_root."""
|
||||||
|
|||||||
Reference in New Issue
Block a user