feat: upgrade interface web e suporte a áudio completo
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
<!-- 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;
|
||||
@@ -26,6 +27,36 @@
|
||||
--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;
|
||||
@@ -318,11 +349,77 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Chat */
|
||||
/* 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;
|
||||
height: min(350px, 45vh);
|
||||
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 {
|
||||
@@ -617,21 +714,52 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Chat AI</div>
|
||||
<div class="card 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?
|
||||
<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="sendMessage()">
|
||||
<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>
|
||||
<div class="chat-input-area">
|
||||
<input type="text" id="chat-input" class="chat-input" placeholder="Digite seu comando..." onkeypress="handleKeyPress(event)">
|
||||
<button class="btn btn-primary" onclick="sendMessage()">
|
||||
<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"/>
|
||||
|
||||
<!-- 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>
|
||||
</button>
|
||||
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>
|
||||
@@ -794,12 +922,36 @@
|
||||
body: JSON.stringify({ text })
|
||||
});
|
||||
const data = await res.json();
|
||||
addBubble(data.reply, 'ai');
|
||||
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();
|
||||
}
|
||||
@@ -880,6 +1032,81 @@
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user