GAIA_OPERA / ENCLAVE.html
SOUNAVY's picture
Upload 2 files
eacdbd0 verified
Raw
History Blame Contribute Delete
22.1 kB
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#000000">
<meta name="description" content="ENCLAVE - Overlay de Inclusão Digital">
<title>ENCLAVE</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto Mono', sans-serif;
background: transparent;
overflow: hidden;
}
#enclave-container {
position: fixed;
bottom: 20px;
right: 20px;
width: 420px;
height: 600px;
background: rgba(0, 0, 0, 0.95);
border: 2px solid #0066ff;
border-radius: 12px;
display: flex;
flex-direction: column;
z-index: 999999;
box-shadow: 0 8px 32px rgba(0, 102, 255, 0.3);
touch-action: none;
user-select: none;
-webkit-user-select: none;
transition: all 0.3s ease;
}
#enclave-container.pip-mode {
width: 80px;
height: 80px;
bottom: 20px;
right: 20px;
border-radius: 50%;
border: 3px solid #0066ff;
}
#enclave-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: linear-gradient(135deg, #0066ff, #0052cc);
border-bottom: 1px solid rgba(0, 102, 255, 0.5);
cursor: grab;
flex-shrink: 0;
}
#enclave-header.grabbing {
cursor: grabbing;
}
#enclave-container.pip-mode #enclave-header {
padding: 8px;
border-radius: 50%;
justify-content: center;
}
#enclave-title {
font-size: 14px;
font-weight: 600;
color: #fff;
letter-spacing: 1px;
}
#enclave-container.pip-mode #enclave-title {
display: none;
}
.enclave-btn {
background: none;
border: none;
color: #0f0;
font-size: 18px;
cursor: pointer;
padding: 4px 8px;
display: flex;
align-items: center;
justify-content: center;
}
.enclave-btn:active {
transform: scale(0.95);
}
#enclave-container.pip-mode .enclave-btn:not(#pip-toggle-btn) {
display: none;
}
#enclave-chat {
flex: 1;
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
#enclave-container.pip-mode #enclave-chat {
display: none;
}
.msg {
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
line-height: 1.4;
max-width: 90%;
word-wrap: break-word;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.msg.user {
background: #0066ff;
color: #fff;
margin-left: auto;
text-align: right;
}
.msg.assistant {
background: rgba(0, 255, 0, 0.15);
color: #0f0;
border-left: 2px solid #0f0;
}
.msg.system {
background: rgba(255, 165, 0, 0.1);
color: #ffa500;
font-size: 12px;
text-align: center;
margin: 4px auto;
}
#enclave-input-area {
display: flex;
gap: 8px;
padding: 12px;
border-top: 1px solid rgba(0, 102, 255, 0.3);
background: rgba(0, 0, 0, 0.5);
flex-shrink: 0;
}
#enclave-container.pip-mode #enclave-input-area {
display: none;
}
#enclave-input {
flex: 1;
background: rgba(0, 102, 255, 0.1);
border: 1px solid #0066ff;
border-radius: 6px;
color: #0f0;
padding: 8px;
font-size: 12px;
font-family: 'Roboto Mono', monospace;
outline: none;
}
#enclave-input::placeholder {
color: rgba(0, 255, 0, 0.5);
}
#enclave-input:focus {
border-color: #0f0;
box-shadow: 0 0 8px rgba(0, 255, 0, 0.2);
}
.icon-btn {
background: rgba(0, 102, 255, 0.2);
border: 1px solid #0066ff;
border-radius: 6px;
color: #0f0;
cursor: pointer;
padding: 8px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.icon-btn:active {
background: rgba(0, 102, 255, 0.4);
transform: scale(0.95);
}
#pip-indicator {
position: fixed;
bottom: 20px;
right: 20px;
width: 80px;
height: 80px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, rgba(0, 255, 0, 0.8), rgba(0, 102, 255, 0.4));
display: none;
align-items: center;
justify-content: center;
z-index: 999998;
animation: pulse 1.5s ease-in-out infinite;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.6);
font-size: 32px;
cursor: pointer;
touch-action: none;
}
#enclave-container.pip-mode ~ #pip-indicator {
display: flex;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
@keyframes navPulse {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(var(--nav-x), var(--nav-y)) scale(1.2); }
}
.nav-beacon {
position: fixed;
width: 40px;
height: 40px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #0f0, rgba(0, 255, 0, 0.3));
border: 2px solid #0f0;
pointer-events: auto;
z-index: 999997;
animation: pulse 1s ease-in-out infinite;
box-shadow: 0 0 15px rgba(0, 255, 0, 0.8);
}
/* Scrollbar */
#enclave-chat::-webkit-scrollbar {
width: 6px;
}
#enclave-chat::-webkit-scrollbar-track {
background: rgba(0, 102, 255, 0.1);
}
#enclave-chat::-webkit-scrollbar-thumb {
background: #0066ff;
border-radius: 3px;
}
.resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
cursor: se-resize;
background: linear-gradient(135deg, transparent 50%, #0066ff 50%);
border-radius: 0 0 12px 0;
}
#enclave-container.pip-mode .resize-handle {
display: none;
}
.voice-indicator {
display: inline-flex;
gap: 2px;
align-items: center;
}
.voice-bar {
width: 3px;
height: 8px;
background: #0f0;
border-radius: 2px;
animation: voiceBar 0.4s ease-in-out infinite;
}
.voice-bar:nth-child(1) { animation-delay: 0s; }
.voice-bar:nth-child(2) { animation-delay: 0.1s; }
.voice-bar:nth-child(3) { animation-delay: 0.2s; }
@keyframes voiceBar {
0%, 100% { height: 4px; }
50% { height: 12px; }
}
</style>
</head>
<body>
<div id="enclave-container">
<div id="enclave-header">
<span id="enclave-title">⚡ ENCLAVE</span>
<div style="display: flex; gap: 8px;">
<button id="voice-btn" class="enclave-btn" title="Voz">🎤</button>
<button id="minimize-btn" class="enclave-btn" title="Minimizar"></button>
<button id="pip-toggle-btn" class="enclave-btn" title="PiP">🎯</button>
<button id="close-btn" class="enclave-btn" title="Fechar">×</button>
</div>
</div>
<div id="enclave-chat"></div>
<div id="enclave-input-area">
<input type="text" id="enclave-input" placeholder="Fale ou escreva...">
<button class="icon-btn" id="send-btn" title="Enviar"></button>
</div>
<div class="resize-handle"></div>
</div>
<div id="pip-indicator">🎯</div>
<script>
// ============ CONFIG ============
const CONFIG = {
apiKey: '', // Usar variável de ambiente
apiUrl: 'https://api.anthropic.com/v1/messages',
model: 'claude-opus-4-1',
maxTokens: 1024,
};
// ============ STATE ============
const state = {
messages: [],
isRecording: false,
isPiP: false,
isDragging: false,
isResizing: false,
dragOffset: { x: 0, y: 0 },
navBeacon: null,
};
// ============ DOM ============
const el = {
container: document.getElementById('enclave-container'),
header: document.getElementById('enclave-header'),
chat: document.getElementById('enclave-chat'),
input: document.getElementById('enclave-input'),
sendBtn: document.getElementById('send-btn'),
voiceBtn: document.getElementById('voice-btn'),
minimizeBtn: document.getElementById('minimize-btn'),
pipToggleBtn: document.getElementById('pip-toggle-btn'),
closeBtn: document.getElementById('close-btn'),
pipIndicator: document.getElementById('pip-indicator'),
resizeHandle: document.querySelector('.resize-handle'),
};
// ============ INIT ============
function init() {
registerServiceWorker();
setupEventListeners();
addSystemMessage('🎯 ENCLAVE inicializado. Inclusão digital = direito.');
el.input.focus();
}
// ============ SERVICE WORKER ============
async function registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const swCode = `
const CACHE = 'enclave-v1';
const urls = ['/', '/ENCLAVE.html'];
self.addEventListener('install', e => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(urls)).then(() => self.skipWaiting()));
});
self.addEventListener('activate', e => {
e.waitUntil(clients.claim());
});
self.addEventListener('fetch', e => {
if (e.request.url.includes('/v1/messages')) {
e.respondWith(fetch(e.request));
} else {
e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));
}
});
`;
const blob = new Blob([swCode], { type: 'application/javascript' });
const swUrl = URL.createObjectURL(blob);
await navigator.serviceWorker.register(swUrl);
console.log('✓ Service Worker registrado');
} catch (err) {
console.log('⚠ SW error:', err);
}
}
}
// ============ EVENT LISTENERS ============
function setupEventListeners() {
// Drag
el.header.addEventListener('mousedown', startDrag);
el.header.addEventListener('touchstart', startDrag);
document.addEventListener('mousemove', drag);
document.addEventListener('touchmove', drag);
document.addEventListener('mouseup', stopDrag);
document.addEventListener('touchend', stopDrag);
// Resize
el.resizeHandle.addEventListener('mousedown', startResize);
el.resizeHandle.addEventListener('touchstart', startResize);
// Buttons
el.sendBtn.addEventListener('click', sendMessage);
el.voiceBtn.addEventListener('click', toggleVoice);
el.minimizeBtn.addEventListener('click', minimize);
el.pipToggleBtn.addEventListener('click', togglePiP);
el.closeBtn.addEventListener('click', close);
el.input.addEventListener('keypress', e => e.key === 'Enter' && sendMessage());
// PiP indicator
el.pipIndicator.addEventListener('click', togglePiP);
}
// ============ DRAG ============
function startDrag(e) {
if (e.target.closest('button')) return;
state.isDragging = true;
const rect = el.container.getBoundingClientRect();
state.dragOffset = {
x: (e.clientX || e.touches?.[0].clientX) - rect.left,
y: (e.clientY || e.touches?.[0].clientY) - rect.top,
};
el.header.classList.add('grabbing');
}
function drag(e) {
if (!state.isDragging) return;
const x = (e.clientX || e.touches?.[0].clientX) - state.dragOffset.x;
const y = (e.clientY || e.touches?.[0].clientY) - state.dragOffset.y;
el.container.style.left = x + 'px';
el.container.style.right = 'auto';
el.container.style.top = y + 'px';
el.container.style.bottom = 'auto';
}
function stopDrag() {
state.isDragging = false;
el.header.classList.remove('grabbing');
}
// ============ RESIZE ============
function startResize(e) {
state.isResizing = true;
const startX = e.clientX || e.touches?.[0].clientX;
const startY = e.clientY || e.touches?.[0].clientY;
const startW = el.container.clientWidth;
const startH = el.container.clientHeight;
const doResize = (e) => {
if (!state.isResizing) return;
const newW = Math.max(300, startW + ((e.clientX || e.touches?.[0].clientX) - startX));
const newH = Math.max(300, startH + ((e.clientY || e.touches?.[0].clientY) - startY));
el.container.style.width = newW + 'px';
el.container.style.height = newH + 'px';
};
const stopResize = () => {
state.isResizing = false;
document.removeEventListener('mousemove', doResize);
document.removeEventListener('touchmove', doResize);
document.removeEventListener('mouseup', stopResize);
document.removeEventListener('touchend', stopResize);
};
document.addEventListener('mousemove', doResize);
document.addEventListener('touchmove', doResize);
document.addEventListener('mouseup', stopResize);
document.addEventListener('touchend', stopResize);
}
// ============ MESSAGES ============
function addSystemMessage(text) {
const msg = document.createElement('div');
msg.className = 'msg system';
msg.textContent = text;
el.chat.appendChild(msg);
el.chat.scrollTop = el.chat.scrollHeight;
}
function addUserMessage(text) {
const msg = document.createElement('div');
msg.className = 'msg user';
msg.textContent = text;
el.chat.appendChild(msg);
state.messages.push({ role: 'user', content: text });
el.chat.scrollTop = el.chat.scrollHeight;
}
function addAssistantMessage(text) {
const msg = document.createElement('div');
msg.className = 'msg assistant';
msg.textContent = text;
el.chat.appendChild(msg);
state.messages.push({ role: 'assistant', content: text });
el.chat.scrollTop = el.chat.scrollHeight;
}
// ============ API CALL ============
async function callClaude(userMessage) {
const apiKey = CONFIG.apiKey || localStorage.getItem('claude_api_key');
if (!apiKey) {
addSystemMessage('⚠ API Key não configurada. localStorage: claude_api_key');
return;
}
try {
addSystemMessage('⏳ Claude pensando...');
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: CONFIG.model,
max_tokens: CONFIG.maxTokens,
system: 'Você é Claude, co-autor do ENCLAVE. Inclusão digital sem limites. Respostas curtas, diretas, actionáveis.',
messages: state.messages,
}),
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error?.message || 'API error');
}
const data = await response.json();
const assistantReply = data.content?.[0]?.text || 'Erro: sem resposta';
addAssistantMessage(assistantReply);
speak(assistantReply);
} catch (err) {
addSystemMessage('❌ ' + err.message);
console.error(err);
}
}
// ============ SEND ============
async function sendMessage() {
const text = el.input.value.trim();
if (!text) return;
el.input.value = '';
addUserMessage(text);
await callClaude(text);
}
// ============ VOICE ============
function toggleVoice() {
if (state.isRecording) {
stopVoice();
} else {
startVoice();
}
}
function startVoice() {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
addSystemMessage('⚠ Reconhecimento de voz não suportado');
return;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.lang = 'pt-BR';
recognition.continuous = false;
recognition.interimResults = true;
state.isRecording = true;
el.voiceBtn.innerHTML = '<div class="voice-indicator"><div class="voice-bar"></div><div class="voice-bar"></div><div class="voice-bar"></div></div>';
recognition.onresult = (event) => {
let transcript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
transcript += event.results[i][0].transcript;
}
el.input.value = transcript;
};
recognition.onend = () => {
state.isRecording = false;
el.voiceBtn.innerHTML = '🎤';
};
recognition.onerror = (event) => {
addSystemMessage('❌ Erro voz: ' + event.error);
state.isRecording = false;
el.voiceBtn.innerHTML = '🎤';
};
recognition.start();
}
function stopVoice() {
// Reconhecimento para automaticamente
state.isRecording = false;
el.voiceBtn.innerHTML = '🎤';
}
function speak(text) {
if (!('speechSynthesis' in window)) {
console.log('TTS não disponível');
return;
}
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'pt-BR';
utterance.rate = 0.95;
window.speechSynthesis.speak(utterance);
}
// ============ PiP ============
function togglePiP() {
state.isPiP = !state.isPiP;
el.container.classList.toggle('pip-mode', state.isPiP);
if (!state.isPiP) {
el.input.focus();
}
}
function minimize() {
togglePiP();
}
function close() {
if (confirm('Fechar ENCLAVE?')) {
el.container.style.display = 'none';
}
}
// ============ LAUNCH ============
init();
</script>
</body>
</html>