1620 lines
63 KiB
HTML
1620 lines
63 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,%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">
|
|
<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 / Modern Browsers */
|
|
@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"] {
|
|
--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);
|
|
}
|
|
|
|
/* Global SVG Presentation */
|
|
svg {
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-width: 2;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
}
|
|
|
|
* {
|
|
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 {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: #060a12;
|
|
z-index: 9999;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.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);
|
|
background: var(--bg-card);
|
|
}
|
|
|
|
.login-icon {
|
|
font-size: 3rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.login-title {
|
|
margin-bottom: 1rem;
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, var(--accent), #8b5cf6);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* Utilities */
|
|
.hidden { display: none !important; }
|
|
.w-full { width: 100%; }
|
|
.text-center { text-align: center; }
|
|
.mt-2 { margin-top: 0.5rem; }
|
|
.opacity-60 { opacity: 0.6; }
|
|
|
|
.theme-icon { width: 18px; height: 18px; }
|
|
.recording-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
background: var(--danger);
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
animation: pulse 1s infinite;
|
|
}
|
|
.mic-active { color: var(--danger) !important; border-color: var(--danger) !important; }
|
|
|
|
.fade-in { animation: fadeIn 0.4s ease-out; }
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* Novas classes para substituir estilos inline */
|
|
.login-error { margin-top: 10px; font-size: 0.75rem; color: var(--danger); font-weight: bold; text-transform: uppercase; }
|
|
.status-bar-info { margin-bottom: 1rem; padding: 0.75rem; background: var(--bg-input); border-radius: 8px; font-size: 0.85rem; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
|
.status-bar-success { margin-bottom: 1rem; padding: 0.5rem 0.75rem; background: var(--success); opacity: 0.15; border-radius: 6px; font-size: 0.75rem; }
|
|
.btn-xs { padding: 0.4rem 0.75rem; font-size: 0.75rem; }
|
|
.config-section-card { padding: 1rem; background: var(--bg-input); border-radius: 8px; border: 1px solid var(--border); }
|
|
.config-section-title { margin-bottom: 0.75rem; font-size: 0.9rem; display: flex; align-items: center; gap: 0.25rem; }
|
|
.config-section-title.accent { color: var(--accent); }
|
|
.config-section-title.success { color: var(--success); }
|
|
.config-section-icon { vertical-align: middle; }
|
|
.mt-4 { margin-top: 1rem; }
|
|
.flex-between { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
|
.text-sm-muted { font-size: 0.85rem; color: var(--text-muted); }
|
|
.scroll-y-auto { overflow-y: auto; }
|
|
.max-h-300 { max-height: 300px; }
|
|
.p-4 { padding: 1rem; }
|
|
.chat-input-container { padding: 0.75rem; gap: 0.5rem; }
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<!-- Login Suspenso -->
|
|
<div id="login-overlay">
|
|
<div class="login-card fade-in">
|
|
<div class="login-icon">🔒</div>
|
|
<h2 class="login-title">BOT VPS TERMINAL</h2>
|
|
<p class="login-desc">DNS restrito. Digite a senha de acesso.</p>
|
|
<input type="password" id="login-pass" class="form-input login-input" placeholder="••••••••" autofocus>
|
|
<button type="button" class="btn btn-primary w-full mt-2" onclick="checkAuth()">ACESSAR HUB</button>
|
|
<div id="login-error" class="hidden login-error">Senha Incorreta</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<div class="logo">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<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 type="button" class="theme-toggle" onclick="toggleTheme()" aria-label="Alternar tema">
|
|
<svg id="icon-sun" class="theme-icon hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<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" class="theme-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<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"></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"></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"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-title">Ações Rápidas</div>
|
|
<div class="actions-grid">
|
|
<button type="button" class="btn" onclick="executeAction('ping')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
|
</svg>
|
|
Ping
|
|
</button>
|
|
<button type="button" class="btn" id="btn-test-llm" onclick="testLLMSpeed()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<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 type="button" class="btn" onclick="executeAction('restart_bot')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<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 type="button" class="btn" onclick="executeAction('clear_cache')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<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 type="button" class="btn btn-danger" onclick="executeAction('reboot_vps')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
|
</svg>
|
|
Reboot VPS
|
|
</button>
|
|
<button type="button" class="btn" id="vnc-toggle-btn" onclick="executeAction('toggle_vnc')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
|
<line x1="8" y1="21" x2="16" y2="21"/>
|
|
<line x1="12" y1="17" x2="12" y2="21"/>
|
|
</svg>
|
|
<span id="vnc-toggle-text">Ligar VNC</span>
|
|
</button>
|
|
<a href="https://vnc.claw.reifonas.cloud/vnc.html" target="_blank" id="vnc-link" class="hidden" style="color: var(--accent); font-size: 0.75rem; text-decoration: none; display: flex; align-items: center; gap: 0.25rem;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
|
Abrir VNC
|
|
</a>
|
|
</div>
|
|
|
|
<div class="section-title">Configuração AI</div>
|
|
<div class="card">
|
|
<div class="config-grid">
|
|
<div class="form-group">
|
|
<label for="active_provider">Provider Ativo</label>
|
|
<select id="active_provider" aria-label="Provider Ativo" class="form-input" onchange="toggleProviderFields()">
|
|
<option value="minimax">MiniMax (Hermes - Externo)</option>
|
|
<option value="openrouter">OpenRouter (Externo)</option>
|
|
<option value="ollama">Ollama (Local-Interno)</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group" id="group-openrouter-model">
|
|
<label for="openrouter_model">Modelo OpenRouter</label>
|
|
<input type="text" id="openrouter_model" class="form-input" placeholder="ex: qwen/qwen-2.5-72b-instruct">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-primary" onclick="saveConfiguration()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<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>
|
|
|
|
<!-- ORCHESTRATOR CONFIG -->
|
|
<div class="section-title">Orquestrador AI (Planner + Executor)</div>
|
|
<div class="card">
|
|
<div class="status-bar-info">
|
|
<strong>Status:</strong> <span id="orchestrator-status">Carregando...</span>
|
|
<button type="button" class="btn btn-xs" onclick="syncFromRepo()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14"><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>
|
|
Sync Repo
|
|
</button>
|
|
<button type="button" class="btn btn-xs" onclick="syncCredentials()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14"><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>
|
|
Sync Local
|
|
</button>
|
|
</div>
|
|
<div class="status-bar-success">
|
|
<strong>Repo Gitea:</strong> <span id="repo-status">Carregando...</span>
|
|
</div>
|
|
|
|
<div class="config-grid">
|
|
<!-- PLANNER CONFIG -->
|
|
<div class="config-section-card">
|
|
<h4 class="config-section-title accent">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" class="config-section-icon"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
|
Planner (Planejador)
|
|
</h4>
|
|
<div class="form-group">
|
|
<label for="planner_provider">Provider</label>
|
|
<select id="planner_provider" aria-label="Provider do Planner" class="form-input" onchange="loadPlannerModels()">
|
|
<option value="gemini">Gemini (Google)</option>
|
|
<option value="openai">OpenAI (GPT-4)</option>
|
|
<option value="anthropic">Anthropic (Claude)</option>
|
|
<option value="ollama">Ollama (Local)</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="planner_model">Modelo</label>
|
|
<select id="planner_model" aria-label="Modelo do Planner" class="form-input">
|
|
<option value="">Carregando...</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- EXECUTOR CONFIG -->
|
|
<div class="config-section-card">
|
|
<h4 class="config-section-title success">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" class="config-section-icon"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
|
Executor (Executor)
|
|
</h4>
|
|
<div class="form-group">
|
|
<label for="executor_provider">Provider</label>
|
|
<select id="executor_provider" aria-label="Provider do Executor" class="form-input" onchange="loadExecutorModels()">
|
|
<option value="ollama" selected>Ollama (Local)</option>
|
|
<option value="gemini">Gemini (Google)</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="executor_model">Modelo</label>
|
|
<select id="executor_model" aria-label="Modelo do Executor" class="form-input">
|
|
<option value="">Carregando...</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<button type="button" class="btn btn-primary" onclick="saveLLMConfig()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<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 Config LLM
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CREDENCIAIS CARREGADAS -->
|
|
<div class="section-title">Credenciais Carregadas</div>
|
|
<div class="card">
|
|
<div class="flex-between">
|
|
<span class="text-sm-muted">
|
|
Credenciais sincronizadas dos serviços (Coolify, Gitea, Supabase, etc)
|
|
</span>
|
|
<button type="button" class="btn btn-xs" onclick="loadCredentials()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14">
|
|
<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>
|
|
Atualizar
|
|
</button>
|
|
</div>
|
|
<div id="credentials-container" class="hidden">
|
|
<div id="credentials-list" class="max-h-300 scroll-y-auto"></div>
|
|
</div>
|
|
<div id="credentials-loading" class="text-center p-4 text-sm-muted">
|
|
Clique em "Atualizar" para carregar as credenciais
|
|
</div>
|
|
</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 chat-input-container">
|
|
<input type="text" id="chat-input" aria-label="Comando do chat" class="chat-input"
|
|
placeholder="Comande sua VPS aqui..." onkeypress="handleKeyPress(event)">
|
|
|
|
<button type="button" class="btn" id="audio-btn" aria-label="Gravar Áudio"
|
|
onclick="toggleRecording()" title="Gravar Áudio">
|
|
<svg id="mic-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<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>
|
|
<span id="recording-dot" class="recording-dot hidden"></span>
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-primary" aria-label="Enviar" onclick="sendChat()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" 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 class="login-icon" role="img" aria-label="Gráfico de Insights">📊</div>
|
|
<p>Aguardando dados estruturados...</p>
|
|
<small class="mt-2 opacity-60">Peça algo como "status dos containers" para ver o refinamento aqui.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<div id="toast">Ação executada!</div>
|
|
|
|
<script>
|
|
let webPassword = '1234'; // Senha padrão suspensa
|
|
let chatHistory = []; // Memória da conversa na Web
|
|
|
|
// 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);
|
|
return res;
|
|
}
|
|
|
|
function initDashboard() {
|
|
fetchStats();
|
|
loadConfig();
|
|
loadOrchestratorStatus();
|
|
loadVNCStatus();
|
|
loadLLMModels().then(function() {
|
|
loadLLMConfig();
|
|
});
|
|
}
|
|
|
|
async function loadVNCStatus() {
|
|
try {
|
|
const res = await apiFetch('/api/vnc_status');
|
|
const data = await res.json();
|
|
const btn = document.getElementById('vnc-toggle-btn');
|
|
const txt = document.getElementById('vnc-toggle-text');
|
|
const lnk = document.getElementById('vnc-link');
|
|
if (data.vnc_status === 'on') {
|
|
txt.textContent = 'Desligar VNC';
|
|
btn.classList.add('btn-success');
|
|
if (lnk) lnk.classList.remove('hidden');
|
|
} else {
|
|
txt.textContent = 'Ligar VNC';
|
|
btn.classList.remove('btn-success');
|
|
if (lnk) lnk.classList.add('hidden');
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
// 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(isLight) {
|
|
const sun = document.getElementById('icon-sun');
|
|
const moon = document.getElementById('icon-moon');
|
|
if (isLight) {
|
|
sun.classList.remove('hidden');
|
|
moon.classList.add('hidden');
|
|
} else {
|
|
sun.classList.add('hidden');
|
|
moon.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
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(() => { 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?',
|
|
toggle_vnc: null
|
|
};
|
|
|
|
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, history: chatHistory })
|
|
});
|
|
const data = await res.json();
|
|
processAIReply(data.reply);
|
|
} catch (e) {
|
|
addBubble('Erro ao contatar servidor.', 'ai');
|
|
}
|
|
}
|
|
|
|
function processAIReply(fullText) {
|
|
// Separa a parte técnica do resumo (RESUMO:)
|
|
const refinedMatch = fullText.match(/RESUMO:\s*([\s\S]*)/i);
|
|
|
|
let technicalPart = fullText;
|
|
if (refinedMatch) {
|
|
technicalPart = fullText.substring(0, refinedMatch.index).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;
|
|
|
|
// Garante que temos a senha (tenta pegar do global ou do localStorage se precisar)
|
|
const activePwd = webPassword || localStorage.getItem('vps_web_password') || '';
|
|
|
|
// Transforma caminhos da VPS ( em links da nossa API segura
|
|
// Ex: /host_root/root/img.jpg -> /api/host_file?pwd=...&path=/host_root/root/img.jpg
|
|
let mdWithAuth = markdown.replace(/!\[(.*?)\]\((\/host_root\/.*?)\)/g, (match, alt, path) => {
|
|
return `}&path=${encodeURIComponent(path)})`;
|
|
});
|
|
|
|
// Fallback para qualquer outro caso que tenha sobrado do prefixo antigo
|
|
mdWithAuth = mdWithAuth.replace(/\/api\/host_file\?path=/g, '/api/host_file?pwd=' + encodeURIComponent(activePwd) + '&path=');
|
|
|
|
// Renderiza o Markdown para HTML usando marked.js
|
|
panel.innerHTML = `<div class="animate-fade-in">${marked.parse(mdWithAuth)}</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;
|
|
|
|
// Atualiza o histórico local (limita a 10 mensagens)
|
|
if (sender === 'user') {
|
|
chatHistory.push({ user: text, bot: "" });
|
|
} else if (chatHistory.length > 0) {
|
|
chatHistory[chatHistory.length - 1].bot = text;
|
|
}
|
|
if (chatHistory.length > 10) chatHistory.shift();
|
|
}
|
|
|
|
async function loadConfig() {
|
|
try {
|
|
const res = await apiFetch('/api/config');
|
|
const data = await res.json();
|
|
const provider = document.getElementById('active_provider');
|
|
const model = document.getElementById('openrouter_model');
|
|
|
|
if (provider) provider.value = data.active_provider || 'minimax';
|
|
if (model) model.value = data.openrouter_model || 'qwen/qwen-2.5-72b-instruct';
|
|
|
|
toggleProviderFields();
|
|
} catch (e) { }
|
|
}
|
|
|
|
function toggleProviderFields() {
|
|
const provider = document.getElementById('active_provider').value;
|
|
const modelGroup = document.getElementById('group-openrouter-model');
|
|
if (modelGroup) {
|
|
modelGroup.style.display = (provider === 'openrouter') ? 'block' : 'none';
|
|
}
|
|
}
|
|
|
|
async function saveConfig() {
|
|
const provider = document.getElementById('active_provider').value;
|
|
const model = document.getElementById('openrouter_model').value.trim();
|
|
|
|
try {
|
|
const res = await apiFetch('/api/config', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ active_provider: provider, openrouter_model: model })
|
|
});
|
|
if (res.ok) {
|
|
showToast('Configurações salvas!');
|
|
loadConfig();
|
|
} else {
|
|
throw new Error();
|
|
}
|
|
} catch (e) {
|
|
showToast('Erro ao salvar.', true);
|
|
}
|
|
}
|
|
|
|
// ================== ORCHESTRATOR LLM CONFIG ==================
|
|
|
|
let availableModels = {};
|
|
|
|
async function loadOrchestratorStatus() {
|
|
try {
|
|
const res = await apiFetch('/api/orchestrator-status');
|
|
const data = await res.json();
|
|
|
|
const statusEl = document.getElementById('orchestrator-status');
|
|
if (statusEl) {
|
|
statusEl.innerHTML = '<strong>Planner:</strong> ' + data.planner.name + ' | <strong>Executor:</strong> ' + data.executor.name + ' | <strong>Ferramentas:</strong> ' + data.available_tools;
|
|
}
|
|
|
|
// Repo status
|
|
const repoEl = document.getElementById('repo-status');
|
|
if (repoEl) {
|
|
const creds = data.credentials || {};
|
|
const giteaRepo = creds.gitea_repo || {};
|
|
if (giteaRepo.available) {
|
|
repoEl.innerHTML = '<span style="color: var(--success);">✅ Online</span> - ' + giteaRepo.services_count + ' serviço(s) disponível(is)';
|
|
} else {
|
|
repoEl.innerHTML = '<span style="color: var(--text-muted);">⏳ Sincronizando...</span>';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
const statusEl = document.getElementById('orchestrator-status');
|
|
if (statusEl) statusEl.textContent = 'Erro ao carregar';
|
|
}
|
|
}
|
|
|
|
async function syncFromRepo() {
|
|
try {
|
|
const res = await apiFetch('/api/sync-from-repo', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (data.status === 'synced') {
|
|
showToast('Repo sincronizado! ' + data.services_count + ' serviços carregados');
|
|
} else {
|
|
showToast('Erro ao sincronizar repo: ' + data.status, true);
|
|
}
|
|
loadOrchestratorStatus();
|
|
loadCredentials();
|
|
} catch (e) {
|
|
showToast('Erro ao sincronizar repo.', true);
|
|
}
|
|
}
|
|
|
|
async function loadLLMModels() {
|
|
try {
|
|
const res = await apiFetch('/api/llm-models');
|
|
availableModels = await res.json();
|
|
} catch (e) {
|
|
console.error('Erro ao carregar modelos:', e);
|
|
}
|
|
}
|
|
|
|
async function loadLLMConfig() {
|
|
try {
|
|
const res = await apiFetch('/api/llm-config');
|
|
const data = await res.json();
|
|
|
|
// Planner
|
|
const plannerProvider = document.getElementById('planner_provider');
|
|
const plannerModel = document.getElementById('planner_model');
|
|
if (plannerProvider) plannerProvider.value = data.planner.provider;
|
|
await loadPlannerModels();
|
|
if (plannerModel && data.planner.model) {
|
|
plannerModel.value = data.planner.model;
|
|
}
|
|
|
|
// Executor
|
|
const executorProvider = document.getElementById('executor_provider');
|
|
const executorModel = document.getElementById('executor_model');
|
|
if (executorProvider) executorProvider.value = data.executor.provider;
|
|
await loadExecutorModels();
|
|
if (executorModel && data.executor.model) {
|
|
executorModel.value = data.executor.model;
|
|
}
|
|
} catch (e) {
|
|
console.error('Erro ao carregar config LLM:', e);
|
|
}
|
|
}
|
|
|
|
function populateModelSelect(selectEl, models) {
|
|
if (!selectEl) return;
|
|
selectEl.innerHTML = '';
|
|
|
|
if (!models || models.length === 0) {
|
|
selectEl.innerHTML = '<option value="">Nenhum modelo</option>';
|
|
return;
|
|
}
|
|
|
|
models.forEach(function(model) {
|
|
const opt = document.createElement('option');
|
|
opt.value = model;
|
|
opt.textContent = model;
|
|
selectEl.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
async function loadPlannerModels() {
|
|
const provider = document.getElementById('planner_provider')?.value;
|
|
const modelSelect = document.getElementById('planner_model');
|
|
if (!modelSelect) return;
|
|
|
|
modelSelect.innerHTML = '<option value="">Carregando...</option>';
|
|
|
|
if (provider === 'ollama') {
|
|
const models = availableModels.models?.find(function(p) { return p.provider === 'ollama'; })?.models || [];
|
|
populateModelSelect(modelSelect, models.length > 0 ? models : ['qwen2.5-coder:1.5b', 'llama3.1:8b', 'codellama:13b']);
|
|
} else {
|
|
const fixedModels = {
|
|
gemini: ['gemini-2.5-flash', 'gemini-2.0-pro', 'gemini-1.5-flash'],
|
|
openai: ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
anthropic: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229']
|
|
};
|
|
populateModelSelect(modelSelect, fixedModels[provider] || []);
|
|
}
|
|
}
|
|
|
|
async function loadExecutorModels() {
|
|
const provider = document.getElementById('executor_provider')?.value;
|
|
const modelSelect = document.getElementById('executor_model');
|
|
if (!modelSelect) return;
|
|
|
|
modelSelect.innerHTML = '<option value="">Carregando...</option>';
|
|
|
|
if (provider === 'ollama') {
|
|
const models = availableModels.models?.find(function(p) { return p.provider === 'ollama'; })?.models || [];
|
|
populateModelSelect(modelSelect, models.length > 0 ? models : ['qwen2.5-coder:1.5b', 'llama3.1:8b', 'codellama:13b']);
|
|
} else if (provider === 'gemini') {
|
|
populateModelSelect(modelSelect, ['gemini-2.5-flash', 'gemini-2.0-pro']);
|
|
} else {
|
|
populateModelSelect(modelSelect, []);
|
|
}
|
|
}
|
|
|
|
async function saveLLMConfig() {
|
|
const plannerProvider = document.getElementById('planner_provider')?.value;
|
|
const plannerModel = document.getElementById('planner_model')?.value;
|
|
const executorProvider = document.getElementById('executor_provider')?.value;
|
|
const executorModel = document.getElementById('executor_model')?.value;
|
|
|
|
try {
|
|
const res = await apiFetch('/api/llm-config', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
planner_provider: plannerProvider,
|
|
planner_model: plannerModel,
|
|
executor_provider: executorProvider,
|
|
executor_model: executorModel
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
showToast('Config LLM salva! Planner: ' + plannerProvider + ', Executor: ' + executorProvider);
|
|
loadOrchestratorStatus();
|
|
} else {
|
|
throw new Error();
|
|
}
|
|
} catch (e) {
|
|
showToast('Erro ao salvar config LLM.', true);
|
|
}
|
|
}
|
|
|
|
async function syncCredentials() {
|
|
try {
|
|
const res = await apiFetch('/api/sync-credentials', {
|
|
method: 'POST'
|
|
});
|
|
const data = await res.json();
|
|
showToast('Credenciais sincronizadas! Services: ' + Object.keys(data.services || {}).length);
|
|
} catch (e) {
|
|
showToast('Erro ao sincronizar.', true);
|
|
}
|
|
}
|
|
|
|
async function loadCredentials() {
|
|
const loadingEl = document.getElementById('credentials-loading');
|
|
const containerEl = document.getElementById('credentials-container');
|
|
const listEl = document.getElementById('credentials-list');
|
|
|
|
if (loadingEl) loadingEl.innerHTML = 'Carregando...';
|
|
|
|
try {
|
|
// Sync first
|
|
await apiFetch('/api/sync-credentials', { method: 'POST' });
|
|
|
|
// Get orchestrator status which has services info
|
|
const res = await apiFetch('/api/orchestrator-status');
|
|
const data = await res.json();
|
|
|
|
const services = data.credentials || {};
|
|
const serviceNames = {
|
|
coolify: 'Coolify (Orquestrador)',
|
|
supabase: 'Supabase (BaaS)',
|
|
gitea: 'Gitea (Git Server)',
|
|
logto: 'Logto (Autenticação)'
|
|
};
|
|
|
|
let html = '<div style="display: grid; gap: 1rem;">';
|
|
|
|
for (const [key, info] of Object.entries(services)) {
|
|
const name = serviceNames[key] || key;
|
|
const status = info.exists ? '<span style="color: var(--success);">Disponivel</span>' : '<span style="color: var(--danger);">Nao disponivel</span>';
|
|
const keysCount = info.keys_count || 0;
|
|
|
|
html += `
|
|
<div style="padding: 1rem; background: var(--bg-input); border-radius: 8px; border: 1px solid var(--border);">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
|
|
<strong style="color: var(--accent);">${name}</strong>
|
|
${status}
|
|
</div>
|
|
<div style="font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.5rem;">
|
|
Caminho: <code style="background: var(--bg-base); padding: 0.1rem 0.3rem; border-radius: 4px;">${info.path || 'N/A'}</code>
|
|
</div>
|
|
<div style="font-size: 0.75rem; color: var(--text-muted);">
|
|
${keysCount} chave(s) encontrada(s)
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
html += '</div>';
|
|
|
|
if (loadingEl) loadingEl.style.display = 'none';
|
|
if (containerEl) containerEl.style.display = 'block';
|
|
if (listEl) listEl.innerHTML = html;
|
|
|
|
} catch (e) {
|
|
if (loadingEl) {
|
|
loadingEl.innerHTML = 'Erro ao carregar credenciais';
|
|
loadingEl.style.color = 'var(--danger)';
|
|
}
|
|
console.error('Erro ao carregar credenciais:', e);
|
|
}
|
|
}
|
|
|
|
async function copyToClipboard(text, label) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
showToast(label + ' copiado!');
|
|
} catch (e) {
|
|
showToast('Erro ao copiar.', 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;
|
|
|
|
// --- 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 btn = document.getElementById('audio-btn');
|
|
|
|
if (active) {
|
|
dot.classList.remove('hidden');
|
|
btn.classList.add('mic-active');
|
|
} else {
|
|
dot.classList.add('hidden');
|
|
btn.classList.remove('mic-active');
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
function checkAuth() {
|
|
const pass = document.getElementById('login-pass').value;
|
|
if (pass === '@@Gi05Br;;') {
|
|
sessionStorage.setItem('botAuth', 'true');
|
|
document.getElementById('login-overlay').classList.add('hidden');
|
|
initDashboard();
|
|
} else {
|
|
document.getElementById('login-error').classList.remove('hidden');
|
|
setTimeout(() => document.getElementById('login-error').classList.add('hidden'), 2000);
|
|
}
|
|
}
|
|
|
|
// Enter key support
|
|
document.getElementById('login-pass').addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') checkAuth();
|
|
});
|
|
|
|
// Skip auth if already logged in via session
|
|
if (sessionStorage.getItem('botAuth') === 'true') {
|
|
document.getElementById('login-overlay').classList.add('hidden');
|
|
initDashboard();
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |