Spaces:
Running
Running
| <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> | |