1129 lines
38 KiB
HTML
1129 lines
38 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="pt-BR" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>VPS AI Dashboard</title>
|
|
<!-- 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 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>
|
|
<style>
|
|
:root {
|
|
--bg-base: #0f172a;
|
|
--bg-card: rgba(30, 41, 59, 0.8);
|
|
--bg-input: rgba(15, 23, 42, 0.5);
|
|
--border: rgba(255, 255, 255, 0.08);
|
|
--accent: #3b82f6;
|
|
--accent-hover: #60a5fa;
|
|
--accent-glow: rgba(59, 130, 246, 0.25);
|
|
--text-main: #f8fafc;
|
|
--text-muted: #94a3b8;
|
|
--danger: #ef4444;
|
|
--danger-bg: rgba(239, 68, 68, 0.15);
|
|
--success: #10b981;
|
|
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
|
--radius: 12px;
|
|
--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 */
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: var(--bg-base);
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--border);
|
|
border-radius: 4px;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: var(--accent);
|
|
}
|
|
|
|
[data-theme="light"] {
|
|
--bg-base: #f1f5f9;
|
|
--bg-card: #ffffff;
|
|
--bg-input: #e2e8f0;
|
|
--border: rgba(0, 0, 0, 0.08);
|
|
--accent: #2563eb;
|
|
--accent-hover: #1d4ed8;
|
|
--accent-glow: rgba(37, 99, 235, 0.2);
|
|
--text-main: #1e3a5f;
|
|
--text-muted: #64748b;
|
|
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
background-color: var(--bg-base);
|
|
color: var(--text-main);
|
|
min-height: 100vh;
|
|
transition: background-color var(--transition), color var(--transition);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 1rem;
|
|
}
|
|
|
|
/* Header */
|
|
header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 0;
|
|
margin-bottom: 1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.logo svg {
|
|
width: 32px;
|
|
height: 32px;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.logo h1 {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, var(--accent), #8b5cf6);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.status-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.4rem 0.75rem;
|
|
background: rgba(16, 185, 129, 0.15);
|
|
color: var(--success);
|
|
border-radius: 20px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-badge::before {
|
|
content: '';
|
|
width: 6px;
|
|
height: 6px;
|
|
background: var(--success);
|
|
border-radius: 50%;
|
|
box-shadow: 0 0 6px var(--success);
|
|
}
|
|
|
|
.status-badge.offline {
|
|
background: var(--danger-bg);
|
|
color: var(--danger);
|
|
}
|
|
|
|
.status-badge.offline::before {
|
|
background: var(--danger);
|
|
box-shadow: 0 0 6px var(--danger);
|
|
}
|
|
|
|
.theme-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 36px;
|
|
height: 36px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all var(--transition);
|
|
color: var(--text-main);
|
|
}
|
|
|
|
.theme-toggle:hover {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.theme-toggle svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
/* Cards */
|
|
.card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 1rem;
|
|
box-shadow: var(--shadow);
|
|
transition: all var(--transition);
|
|
}
|
|
|
|
/* Stats Grid */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 0.75rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.stat-card h3 {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--text-muted);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.progress-bar-bg {
|
|
height: 6px;
|
|
background: var(--bg-input);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 100%;
|
|
background: var(--accent);
|
|
border-radius: 3px;
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.progress-bar.warning {
|
|
background: #f59e0b;
|
|
}
|
|
|
|
.progress-bar.danger {
|
|
background: var(--danger);
|
|
}
|
|
|
|
/* Section Title */
|
|
.section-title {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
margin: 1.25rem 0 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* Actions Grid */
|
|
.actions-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.65rem 1rem;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
color: var(--text-main);
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all var(--transition);
|
|
}
|
|
|
|
.btn svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.btn:hover {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px var(--accent-glow);
|
|
}
|
|
|
|
.btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.btn-danger {
|
|
border-color: rgba(239, 68, 68, 0.3);
|
|
color: var(--danger);
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: var(--danger);
|
|
border-color: var(--danger);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: var(--accent-hover);
|
|
}
|
|
|
|
/* Config Form */
|
|
.config-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--text-muted);
|
|
margin-bottom: 0.35rem;
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 0.6rem 0.75rem;
|
|
background: var(--bg-input);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
color: var(--text-main);
|
|
font-size: 0.85rem;
|
|
transition: border-color var(--transition);
|
|
}
|
|
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
select.form-input {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Chat & Insights Layout */
|
|
.chat-layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1rem;
|
|
height: clamp(400px, 60vh, 800px);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.chat-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.insights-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg-card);
|
|
border: 2px solid var(--accent-glow);
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
box-shadow: 0 0 20px rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.insights-header {
|
|
padding: 0.75rem 1rem;
|
|
background: rgba(59, 130, 246, 0.05);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.insights-content {
|
|
flex: 1;
|
|
padding: 1.25rem;
|
|
overflow-y: auto;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.insights-content h3 { margin-bottom: 1rem; color: var(--accent); }
|
|
.insights-content table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 1rem 0;
|
|
background: var(--bg-input);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.insights-content th, .insights-content td {
|
|
padding: 0.75rem;
|
|
text-align: left;
|
|
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-placeholder {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-muted);
|
|
text-align: center;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0.75rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.chat-bubble {
|
|
max-width: 80%;
|
|
padding: 0.6rem 0.9rem;
|
|
border-radius: 12px;
|
|
font-size: 0.85rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.bubble-ai {
|
|
background: rgba(59, 130, 246, 0.1);
|
|
border: 1px solid rgba(59, 130, 246, 0.2);
|
|
align-self: flex-start;
|
|
border-bottom-left-radius: 4px;
|
|
}
|
|
|
|
.bubble-user {
|
|
background: var(--accent);
|
|
color: white;
|
|
align-self: flex-end;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
|
|
.chat-input-area {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
padding-top: 0.75rem;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.chat-input {
|
|
flex: 1;
|
|
padding: 0.6rem 0.75rem;
|
|
background: var(--bg-input);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
color: var(--text-main);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.chat-input:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.chat-input::placeholder {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Toast */
|
|
#toast {
|
|
position: fixed;
|
|
bottom: 1rem;
|
|
right: 1rem;
|
|
padding: 0.75rem 1.25rem;
|
|
background: var(--success);
|
|
color: white;
|
|
border-radius: 8px;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
transition: all 0.3s ease;
|
|
pointer-events: none;
|
|
z-index: 1000;
|
|
}
|
|
|
|
#toast.show {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
#toast.error {
|
|
background: var(--danger);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 640px) {
|
|
.container {
|
|
padding: 0.75rem;
|
|
}
|
|
|
|
header {
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.header-actions {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.actions-grid {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
|
|
.config-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.chat-wrapper {
|
|
height: 300px;
|
|
}
|
|
|
|
.chat-bubble {
|
|
max-width: 90%;
|
|
}
|
|
|
|
.btn {
|
|
padding: 0.6rem 0.75rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 380px) {
|
|
.actions-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* Login Overlay */
|
|
#login-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--bg-base);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 2000;
|
|
backdrop-filter: blur(20px);
|
|
transition: opacity 0.5s ease, visibility 0.5s;
|
|
}
|
|
|
|
#login-overlay.hidden {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.login-card {
|
|
width: 90%;
|
|
max-width: 400px;
|
|
padding: 2.5rem;
|
|
text-align: center;
|
|
border: 1px solid var(--accent);
|
|
box-shadow: 0 0 30px var(--accent-glow);
|
|
}
|
|
|
|
.login-card h2 {
|
|
margin-bottom: 1rem;
|
|
font-size: 1.5rem;
|
|
background: linear-gradient(135deg, var(--accent), #8b5cf6);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="login-overlay">
|
|
<div class="card login-card">
|
|
<div style="font-size: 3rem; margin-bottom: 1rem;">🔒</div>
|
|
<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>
|
|
<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()">
|
|
<button class="btn btn-primary" style="width:100%; padding: 0.8rem;" onclick="attemptLogin()">Entrar no Dashboard</button>
|
|
<div id="login-error" style="color:var(--danger); font-size: 0.75rem; margin-top: 1rem; display: none;">Senha incorreta. Tente novamente.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<div class="logo">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
|
</svg>
|
|
<h1>VPS AI Dashboard</h1>
|
|
</div>
|
|
<div class="header-actions">
|
|
<div class="status-badge" id="bot-status">Online</div>
|
|
<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;">
|
|
<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"/>
|
|
</svg>
|
|
<svg id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="stats-grid">
|
|
<div class="card stat-card">
|
|
<h3>CPU</h3>
|
|
<div class="stat-value" id="cpu-val">--%</div>
|
|
<div class="progress-bar-bg">
|
|
<div class="progress-bar" id="cpu-bar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<h3>RAM</h3>
|
|
<div class="stat-value" id="ram-val">-- / -- GB</div>
|
|
<div class="progress-bar-bg">
|
|
<div class="progress-bar" id="ram-bar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<h3>Disk</h3>
|
|
<div class="stat-value" id="disk-val">--%</div>
|
|
<div class="progress-bar-bg">
|
|
<div class="progress-bar" id="disk-bar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-title">Ações Rápidas</div>
|
|
<div class="actions-grid">
|
|
<button class="btn" onclick="executeAction('ping')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
|
</svg>
|
|
Ping
|
|
</button>
|
|
<button class="btn" id="btn-test-llm" onclick="testLLMSpeed()">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<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 12V2.5l5.5 5.5"/>
|
|
</svg>
|
|
Testar LLM
|
|
</button>
|
|
<button class="btn" onclick="executeAction('restart_bot')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<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"/>
|
|
</svg>
|
|
Reiniciar
|
|
</button>
|
|
<button class="btn" onclick="executeAction('clear_cache')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<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>
|
|
Limpar Cache
|
|
</button>
|
|
<button class="btn btn-danger" onclick="executeAction('reboot_vps')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
|
</svg>
|
|
Reboot VPS
|
|
</button>
|
|
</div>
|
|
|
|
<div class="section-title">Configuração AI</div>
|
|
<div class="card">
|
|
<div class="config-grid">
|
|
<div class="form-group">
|
|
<label>Provider Ativo</label>
|
|
<select id="active_provider" class="form-input">
|
|
<option value="ollama">Ollama (Local)</option>
|
|
<option value="gemini">Gemini Pro (Google)</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Gemini API Key</label>
|
|
<input type="password" id="gemini_api_key" class="form-input" placeholder="AIzaSy...">
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="saveConfiguration()">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<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="7 3 7 8 15 8"/>
|
|
</svg>
|
|
Salvar
|
|
</button>
|
|
</div>
|
|
|
|
<div class="section-title">Terminal & Insights da IA</div>
|
|
<div class="chat-layout">
|
|
<!-- Coluna 1: Chat Técnico -->
|
|
<div class="chat-wrapper">
|
|
<div class="chat-messages" id="chat-box">
|
|
<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.
|
|
</div>
|
|
</div>
|
|
<div class="chat-input-area" style="padding: 0.75rem; gap: 0.5rem;">
|
|
<input type="text" id="chat-input" 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">
|
|
<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>
|
|
<div id="recording-dot" style="display:none; width:8px; height:8px; background:red; border-radius:50%; animation: pulse 1s infinite;"></div>
|
|
</button>
|
|
|
|
<button class="btn btn-primary" onclick="sendChat()">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="22" y1="2" x2="11" y2="13"/>
|
|
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Coluna 2: Painel de Insights (Refinado) -->
|
|
<div class="insights-wrapper">
|
|
<div class="insights-header">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
|
<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="2" x2="16" y2="12"/>
|
|
</svg>
|
|
Painel de Insights Visuais
|
|
</div>
|
|
<div class="insights-content" id="insights-panel">
|
|
<div class="insights-placeholder">
|
|
<div style="font-size: 2.5rem; margin-bottom: 1rem;">📊</div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="toast">Ação executada!</div>
|
|
|
|
<script>
|
|
let webPassword = localStorage.getItem('vps_web_password') || '';
|
|
|
|
// Helper para chamadas de API com autenticação
|
|
async function apiFetch(url, options = {}) {
|
|
if (!options.headers) options.headers = {};
|
|
options.headers['X-Web-Password'] = webPassword;
|
|
|
|
const res = await fetch(url, options);
|
|
if (res.status === 401) {
|
|
showLoginOverlay();
|
|
throw new Error("Não autorizado");
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function showLoginOverlay() {
|
|
document.getElementById('login-overlay').classList.remove('hidden');
|
|
document.getElementById('login-error').style.display = 'none';
|
|
}
|
|
|
|
async function attemptLogin() {
|
|
const input = document.getElementById('web-pass-input');
|
|
const pwd = input.value.trim();
|
|
if (!pwd) return;
|
|
|
|
const res = await fetch('/api/login', {
|
|
headers: { 'X-Web-Password': pwd }
|
|
});
|
|
|
|
if (res.ok) {
|
|
webPassword = pwd;
|
|
localStorage.setItem('vps_web_password', pwd);
|
|
document.getElementById('login-overlay').classList.add('hidden');
|
|
initDashboard();
|
|
} else {
|
|
document.getElementById('login-error').style.display = 'block';
|
|
input.value = '';
|
|
input.focus();
|
|
}
|
|
}
|
|
|
|
function initDashboard() {
|
|
fetchStats();
|
|
loadConfig();
|
|
}
|
|
|
|
// Theme management
|
|
function toggleTheme() {
|
|
const html = document.documentElement;
|
|
const isDark = html.dataset.theme !== 'light';
|
|
html.dataset.theme = isDark ? 'light' : 'dark';
|
|
localStorage.setItem('theme', html.dataset.theme);
|
|
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');
|
|
if (savedTheme) {
|
|
document.documentElement.dataset.theme = savedTheme;
|
|
updateThemeIcon(savedTheme === 'light');
|
|
}
|
|
|
|
// Stats
|
|
async function fetchStats() {
|
|
try {
|
|
const res = await apiFetch('/api/status');
|
|
const data = await res.json();
|
|
|
|
const cpuVal = document.getElementById('cpu-val');
|
|
const cpuBar = document.getElementById('cpu-bar');
|
|
if (cpuVal) cpuVal.textContent = data.cpu + '%';
|
|
if (cpuBar) {
|
|
cpuBar.style.width = data.cpu + '%';
|
|
cpuBar.className = 'progress-bar' + (data.cpu > 80 ? ' danger' : data.cpu > 60 ? ' warning' : '');
|
|
}
|
|
|
|
const ramVal = document.getElementById('ram-val');
|
|
const ramBar = document.getElementById('ram-bar');
|
|
if (ramVal) ramVal.textContent = data.ram.used + ' / ' + data.ram.total + ' GB';
|
|
if (ramBar) ramBar.style.width = data.ram.percent + '%';
|
|
|
|
const diskVal = document.getElementById('disk-val');
|
|
const diskBar = document.getElementById('disk-bar');
|
|
if (diskVal) diskVal.textContent = data.disk.percent + '%';
|
|
if (diskBar) {
|
|
diskBar.style.width = data.disk.percent + '%';
|
|
diskBar.className = 'progress-bar' + (data.disk.percent > 90 ? ' danger' : data.disk.percent > 75 ? ' warning' : '');
|
|
}
|
|
|
|
const status = document.getElementById('bot-status');
|
|
if (status) {
|
|
status.textContent = 'Online';
|
|
status.classList.remove('offline');
|
|
}
|
|
} catch (e) {
|
|
const status = document.getElementById('bot-status');
|
|
if (status) {
|
|
status.textContent = 'Offline';
|
|
status.classList.add('offline');
|
|
}
|
|
}
|
|
}
|
|
|
|
setInterval(() => { if(!document.getElementById('login-overlay').classList.contains('hidden')) return; fetchStats(); }, 3000);
|
|
|
|
// Actions
|
|
async function executeAction(type) {
|
|
const messages = {
|
|
reboot_vps: '⚠️ Confirma reboot CRÍTICO da VPS?',
|
|
clear_cache: 'Limpar cache do servidor?',
|
|
restart_bot: 'Reiniciar o agente AI?'
|
|
};
|
|
|
|
if (messages[type] && !confirm(messages[type])) return;
|
|
|
|
try {
|
|
const res = await apiFetch('/api/action', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ type })
|
|
});
|
|
const data = await res.json();
|
|
showToast(data.message || 'Ação executada!');
|
|
} catch (e) {
|
|
showToast('Erro ao executar.', true);
|
|
}
|
|
}
|
|
|
|
function showToast(msg, isError = false) {
|
|
const toast = document.getElementById('toast');
|
|
if(!toast) return;
|
|
toast.textContent = msg;
|
|
toast.className = isError ? 'error show' : 'show';
|
|
setTimeout(() => toast.classList.remove('show'), 3000);
|
|
}
|
|
|
|
async function sendChat() {
|
|
const input = document.getElementById('chat-input');
|
|
const text = input.value.trim();
|
|
if (!text) return;
|
|
|
|
addBubble(text, 'user');
|
|
input.value = '';
|
|
|
|
try {
|
|
const res = await apiFetch('/api/chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ text })
|
|
});
|
|
const data = await res.json();
|
|
processAIReply(data.reply);
|
|
} catch (e) {
|
|
addBubble('Erro ao contatar servidor.', 'ai');
|
|
}
|
|
}
|
|
|
|
function processAIReply(fullText) {
|
|
// Separa a parte técnica da parte refinada
|
|
const refinedMatch = fullText.match(/<REFINED>([\s\S]*?)<\/REFINED>/i);
|
|
|
|
let technicalPart = fullText;
|
|
if (refinedMatch) {
|
|
technicalPart = fullText.replace(refinedMatch[0], '').trim();
|
|
const refinedContent = refinedMatch[1].trim();
|
|
updateInsightsPanel(refinedContent);
|
|
}
|
|
|
|
if (technicalPart) {
|
|
addBubble(technicalPart, 'ai');
|
|
}
|
|
}
|
|
|
|
function updateInsightsPanel(markdown) {
|
|
const panel = document.getElementById('insights-panel');
|
|
if(!panel) return;
|
|
|
|
// Renderiza o Markdown para HTML usando marked.js
|
|
panel.innerHTML = `<div class="animate-fade-in">${marked.parse(markdown)}</div>`;
|
|
}
|
|
|
|
function handleKeyPress(e) {
|
|
if (e.key === 'Enter') sendChat();
|
|
}
|
|
|
|
function addBubble(text, sender) {
|
|
const box = document.getElementById('chat-box');
|
|
if(!box) return;
|
|
const div = document.createElement('div');
|
|
div.className = 'chat-bubble bubble-' + sender;
|
|
div.textContent = text;
|
|
box.appendChild(div);
|
|
box.scrollTop = box.scrollHeight;
|
|
}
|
|
|
|
async function loadConfig() {
|
|
try {
|
|
const res = await apiFetch('/api/config');
|
|
const data = await res.json();
|
|
const provider = document.getElementById('active_provider');
|
|
const key = document.getElementById('gemini_api_key');
|
|
if (provider) provider.value = data.active_provider || 'ollama';
|
|
if (key) key.value = data.gemini_api_key || '';
|
|
} catch (e) {}
|
|
}
|
|
|
|
async function saveConfig() {
|
|
const provider = document.getElementById('active_provider').value;
|
|
const key = document.getElementById('gemini_api_key').value.trim();
|
|
|
|
try {
|
|
const res = await apiFetch('/api/config', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ active_provider: provider, gemini_api_key: key })
|
|
});
|
|
if (res.ok) {
|
|
showToast('Configurações salvas!');
|
|
loadConfig();
|
|
} else {
|
|
throw new Error();
|
|
}
|
|
} catch (e) {
|
|
showToast('Erro ao salvar.', true);
|
|
}
|
|
}
|
|
|
|
async function testLLMSpeed() {
|
|
const btn = document.getElementById('btn-test-llm');
|
|
const originalContent = btn.innerHTML;
|
|
btn.innerHTML = '⚡ Testando...';
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
const res = await apiFetch('/api/test_llm');
|
|
const data = await res.json();
|
|
if(data.status === 'success') {
|
|
showToast(`✅ LLM Online! Resposta em ${data.latency}s`);
|
|
btn.innerHTML = `✅ ${data.latency}s`;
|
|
setTimeout(() => { btn.innerHTML = originalContent; btn.disabled = false; }, 5000);
|
|
} else {
|
|
throw new Error(data.message);
|
|
}
|
|
} catch(e) {
|
|
showToast("❌ Erro no Teste LLM: " + e.message, true);
|
|
btn.innerHTML = '❌ Falhou';
|
|
btn.classList.add('btn-danger');
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalContent;
|
|
btn.disabled = false;
|
|
btn.classList.remove('btn-danger');
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
window.saveConfiguration = saveConfig;
|
|
window.sendMessage = sendChat;
|
|
window.executeAction = executeAction;
|
|
window.testLLMSpeed = testLLMSpeed;
|
|
window.attemptLogin = attemptLogin;
|
|
|
|
// --- AUDIO SYSTEM ---
|
|
let mediaRecorder;
|
|
let audioChunks = [];
|
|
let isRecording = false;
|
|
|
|
async function toggleRecording() {
|
|
if (!isRecording) {
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
mediaRecorder = new MediaRecorder(stream);
|
|
audioChunks = [];
|
|
|
|
mediaRecorder.ondataavailable = (event) => {
|
|
audioChunks.push(event.data);
|
|
};
|
|
|
|
mediaRecorder.onstop = async () => {
|
|
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
|
await uploadAudio(audioBlob);
|
|
};
|
|
|
|
mediaRecorder.start();
|
|
isRecording = true;
|
|
updateMicUI(true);
|
|
} catch (err) {
|
|
showToast("Erro ao acessar microfone", true);
|
|
}
|
|
} else {
|
|
mediaRecorder.stop();
|
|
mediaRecorder.stream.getTracks().forEach(t => t.stop());
|
|
isRecording = false;
|
|
updateMicUI(false);
|
|
}
|
|
}
|
|
|
|
function updateMicUI(active) {
|
|
const dot = document.getElementById('recording-dot');
|
|
const icon = document.getElementById('mic-icon');
|
|
const btn = document.getElementById('audio-btn');
|
|
|
|
dot.style.display = active ? 'block' : 'none';
|
|
icon.style.color = active ? 'red' : 'inherit';
|
|
btn.style.borderColor = active ? 'red' : 'var(--border)';
|
|
}
|
|
|
|
async function uploadAudio(blob) {
|
|
const formData = new FormData();
|
|
formData.append('audio', blob, 'voice.webm');
|
|
|
|
showToast("✨ Transcrevendo áudio...");
|
|
|
|
try {
|
|
const res = await apiFetch('/api/chat-audio', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
const data = await res.json();
|
|
|
|
if(data.text) {
|
|
addBubble(data.text, 'user');
|
|
processAIReply(data.reply);
|
|
|
|
// Se o bot retornou áudio, toca ele
|
|
if(data.audio_url) {
|
|
const audio = new Audio(data.audio_url + '?t=' + Date.now());
|
|
audio.play();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
showToast("Erro ao processar áudio", true);
|
|
}
|
|
}
|
|
|
|
window.toggleRecording = toggleRecording;
|
|
|
|
// Auto-login se já tiver senha salva
|
|
if (webPassword) {
|
|
(async () => {
|
|
try {
|
|
const res = await fetch('/api/login', { headers: { 'X-Web-Password': webPassword } });
|
|
if (res.ok) {
|
|
document.getElementById('login-overlay').classList.add('hidden');
|
|
initDashboard();
|
|
} else {
|
|
showLoginOverlay();
|
|
}
|
|
} catch(e) { showLoginOverlay(); }
|
|
})();
|
|
} else {
|
|
showLoginOverlay();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|