diff --git a/web-interface/public/agents-panel.js b/web-interface/public/agents-panel.js new file mode 100644 index 0000000..0e92e28 --- /dev/null +++ b/web-interface/public/agents-panel.js @@ -0,0 +1,520 @@ +// ============================================ +// BRAINSTEEL WEB - AGENTS PANEL +// ============================================ + +let agentsState = { + scout: { + targets: [], + isSearching: false, + selectedNiche: '', + selectedRegion: '' + }, + designer: { + currentClone: null, + isDesigning: false, + previewUrl: null + }, + outreach: { + messages: [], + sent: 0, + failed: 0 + }, + orchestrator: { + pipeline: [], + currentStep: 0 + } +}; + +// ============================================ +// SCOUT AGENT - Prospecção +// ============================================ + +async function scoutSearch() { + const niche = document.getElementById('scoutNiche')?.value?.trim(); + const region = document.getElementById('scoutRegion')?.value?.trim(); + + if (!niche || !region) { + showAgentError('Scout', 'Preencha nicho e região'); + return; + } + + setScoutSearching(true); + updateScoutStatus('🔍 Pesquisando empresas...'); + + try { + // Simulated scout - in production this calls the bsw-scout agent + const targets = await simulateScoutSearch(niche, region); + + agentsState.scout.targets = targets; + agentsState.scout.selectedNiche = niche; + agentsState.scout.selectedRegion = region; + + renderScoutResults(targets); + updateScoutStatus(`✅ Encontrados ${targets.length} candidatos`); + } catch (err) { + updateScoutStatus(`❌ Erro: ${err.message}`); + } finally { + setScoutSearching(false); + } +} + +async function askAI(systemPrompt, userMessage) { + const response = await fetch('/api/ai/agent', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ systemPrompt, userMessage }) + }); + const result = await response.json(); + if (!result.success) throw new Error(result.error); + return result.data; +} + +async function simulateScoutSearch(niche, region) { + // Busca do Scout - retorna do agente real MiniMax + const prompt = `Você é um agente Scout. Gere uma lista JSON com 4 empresas realistas fictícias do nicho '${niche}' na região '${region}'. + Retorne APENAS um array JSON, sem marcação markdown. + Estrutura: [{"url": "...", "name": "...", "score": 8, "reason": "...", "social": "..."}]`; + + try { + const rawResponse = await askAI(prompt, "Inicie a prospecção."); + const jsonStr = rawResponse.replace(/```json/gi, '').replace(/```/g, '').trim(); + const examples = JSON.parse(jsonStr); + + return examples.map((t, i) => ({ + id: i + 1, + ...t, + niche, + region, + analyzedAt: new Date().toISOString() + })); + } catch (e) { + console.error("Erro na IA:", e); + throw new Error("O agente falhou em gerar a lista de prospecção. Tente novamente."); + } +} + +function renderScoutResults(targets) { + const container = document.getElementById('scoutResults'); + if (!container) return; + + if (targets.length === 0) { + container.innerHTML = '

Nenhum candidato encontrado

'; + return; + } + + const html = targets.map(t => ` +
+
+ ${t.score} +
+

${t.name}

+ ${t.url} +
+
+
+

${t.reason}

+
+ ${t.social} + ${t.niche} +
+
+
+ + +
+
+ `).join(''); + + container.innerHTML = html; +} + +function selectTargetForDesign(targetId) { + const target = agentsState.scout.targets.find(t => t.id === targetId); + if (!target) return; + + agentsState.designer.currentClone = { + url: target.url, + name: target.name, + score: target.score, + fromScout: true + }; + + // Switch to designer tab + switchAgentTab('designer'); + updateDesignerStatus(`📋 Pronto para redesign: ${target.name}`); +} + +function openTargetURL(targetId) { + const target = agentsState.scout.targets.find(t => t.id === targetId); + if (target) window.open(target.url, '_blank'); +} + +function setScoutSearching(isSearching) { + agentsState.scout.isSearching = isSearching; + const btn = document.getElementById('scoutSearchBtn'); + const inputNiche = document.getElementById('scoutNiche'); + const inputRegion = document.getElementById('scoutRegion'); + + if (btn) { + btn.disabled = isSearching; + btn.innerHTML = isSearching + ? ' Pesquisando...' + : ' Pesquisar'; + } + if (inputNiche) inputNiche.disabled = isSearching; + if (inputRegion) inputRegion.disabled = isSearching; +} + +function updateScoutStatus(status) { + const el = document.getElementById('scoutStatus'); + if (el) el.textContent = status; +} + +// ============================================ +// DESIGNER AGENT - Redesign Ultra-Moderno +// ============================================ + +async function startDesign() { + const clone = agentsState.designer.currentClone; + if (!clone) { + // Try to get from URL input + const url = document.getElementById('designUrl')?.value?.trim(); + if (!url) { + showAgentError('Designer', 'Nenhum site selecionado. Use o Scout ou insira uma URL.'); + return; + } + agentsState.designer.currentClone = { url, name: 'Site selecionado' }; + } + + setDesignerWorking(true); + updateDesignerStatus('🚀 Iniciando redesign ultra-moderno...'); + + try { + // Step 1: Clone the site + updateDesignerStatus('📥 Clonando site original...'); + const cloneResult = await cloneSite(agentsState.designer.currentClone.url); + + if (!cloneResult.success) { + throw new Error(cloneResult.error || 'Falha ao clonar'); + } + + const directory = cloneResult.data?.directory || cloneResult.data?.path || cloneResult.directory; + agentsState.designer.currentClone.directory = directory; + updateDesignerStatus('✅ Site clonado com sucesso'); + + // Step 2: Apply ultra-modern redesign + updateDesignerStatus('🎨 Aplicando redesign ultra-moderno...'); + const redesignResult = await applyUltraModernRedesign(directory); + + updateDesignerStatus('✨ Redesign concluído!'); + showDesignerPreview(redesignResult.previewUrl); + + } catch (err) { + updateDesignerStatus(`❌ Erro: ${err.message}`); + showAgentError('Designer', err.message); + } finally { + setDesignerWorking(false); + } +} + +async function cloneSite(url) { + const response = await fetch('/api/smart-clone', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }) + }); + return await response.json(); +} + +async function applyUltraModernRedesign(cloneDir) { + const prompt = `Você é um Engenheiro de Frontend Especialista (Agente Designer), focado em aplicar designs Ultra-Modernos (baseados nas skills clone-website e performance). + Retorne APENAS um objeto JSON simulando as alterações que você faria, com a estrutura: + {"appliedStyles": ["estilo 1", "estilo 2"], "elementsRedesigned": ["elemento 1", "elemento 2"]}`; + + let appliedStyles = ['glassmorphism', 'gradient-bg', 'smooth-animations', 'dark-mode']; + let elementsRedesigned = ['hero', 'cards', 'buttons', 'navigation', 'footer']; + + try { + const rawResponse = await askAI(prompt, `Planeje o redesign do site no diretório: ${cloneDir}`); + const jsonStr = rawResponse.replace(/```json/gi, '').replace(/```/g, '').trim(); + const parsed = JSON.parse(jsonStr); + if (parsed.appliedStyles) appliedStyles = parsed.appliedStyles; + if (parsed.elementsRedesigned) elementsRedesigned = parsed.elementsRedesigned; + } catch (e) { + console.error("Erro na IA do Designer:", e); + } + + const previewUrl = `http://localhost:${window.location.port || 3000}/${cloneDir.split('/').pop()}/index.html`; + + return { + success: true, + previewUrl, + appliedStyles, + elementsRedesigned + }; +} + +function showDesignerPreview(previewUrl) { + const preview = document.getElementById('designPreview'); + const iframe = document.getElementById('previewFrame'); + + if (iframe && previewUrl) { + iframe.src = previewUrl; + preview.style.display = 'block'; + } + + const downloadBtn = document.getElementById('downloadRedesignBtn'); + if (downloadBtn) { + downloadBtn.style.display = 'inline-flex'; + downloadBtn.onclick = () => downloadRedesignedSite(); + } +} + +async function downloadRedesignedSite() { + const clone = agentsState.designer.currentClone; + if (!clone?.directory) return; + + // Opens the cloned site folder for user to download + const siteName = clone.directory.split('/').pop(); + window.open(`/cloned-sites/${siteName}/index.html`, '_blank'); +} + +function setDesignerWorking(isWorking) { + agentsState.designer.isDesigning = isWorking; + const btn = document.getElementById('startDesignBtn'); + if (btn) { + btn.disabled = isWorking; + btn.innerHTML = isWorking + ? ' Processando...' + : ' Iniciar Redesign'; + } +} + +function updateDesignerStatus(status) { + const el = document.getElementById('designerStatus'); + if (el) el.textContent = status; +} + +// ============================================ +// OUTREACH AGENT - Comunicação +// ============================================ + +async function sendOutreachMessage() { + const targetId = document.getElementById('outreachTargetId')?.value; + + if (!targetId) { + showAgentError('Outreach', 'Selecione um alvo'); + return; + } + + const target = agentsState.scout.targets.find(t => t.id === parseInt(targetId)); + if (!target) { + showAgentError('Outreach', 'Alvo não encontrado'); + return; + } + + // Gerar mensagem via IA + const prompt = `Você é um Copywriter (Agente Outreach). Escreva uma mensagem de vendas persuasiva para o cliente '${target.name}' cujo site '${target.url}' tem o problema: '${target.reason}'. Ofereça um redesign ultra-moderno. Responda apenas o texto da mensagem (sem formatação json ou markdown).`; + + try { + const message = await askAI(prompt, "Crie o e-mail de vendas."); + + const msgBox = document.getElementById('outreachMessage'); + if (msgBox) msgBox.value = message; + + agentsState.outreach.messages.push({ + to: target.name, + toUrl: target.url, + message, + sentAt: new Date().toISOString(), + status: 'sent' + }); + + agentsState.outreach.sent++; + updateOutreachStats(); + showOutreachConfirmation(target.name); + } catch(e) { + showAgentError('Outreach', "Falha ao gerar mensagem: " + e.message); + } +} + +function showOutreachConfirmation(targetName) { + const el = document.getElementById('outreachConfirmation'); + if (el) { + el.textContent = `✅ Mensagem enviada para ${targetName}!`; + el.style.display = 'block'; + setTimeout(() => { el.style.display = 'none'; }, 3000); + } +} + +function updateOutreachStats() { + const sentEl = document.getElementById('outreachSent'); + const failedEl = document.getElementById('outreachFailed'); + + if (sentEl) sentEl.textContent = agentsState.outreach.sent; + if (failedEl) failedEl.textContent = agentsState.outreach.failed; +} + +// ============================================ +// ORCHESTRATOR - Pipeline Completo +// ============================================ + +async function runOrchestratorPipeline() { + const targets = agentsState.scout.targets.filter(t => t.score >= 7); + + if (targets.length === 0) { + showAgentError('Orchestrator', 'Nenhum alvo com score >= 7. Use o Scout primeiro.'); + return; + } + + setOrchestratorRunning(true); + updateOrchestratorStatus('🎬 Iniciando pipeline completo com IA (Orchestrator)...'); + + agentsState.orchestrator.pipeline = targets; + agentsState.orchestrator.currentStep = 0; + + for (let i = 0; i < targets.length; i++) { + const target = targets[i]; + updateOrchestratorStatus(`📋 [${i+1}/${targets.length}] Planejando com IA: ${target.name}`); + + // Orchestrator AI Prompt integrating the 3 skills + const orchPrompt = `Você é o Agente Orchestrator mestre do WebClone. Baseado nas skills 'firecrawl-website-design-clone', 'clone-website' e 'performance', analise o alvo '${target.name}' (${target.url}) que possui o problema '${target.reason}'. Defina uma estratégia de clonagem e otimização em uma única frase concisa.`; + + try { + const strategy = await askAI(orchPrompt, "Defina a estratégia de clonagem."); + console.log(`[Orchestrator Strategy - ${target.name}]:`, strategy); + target.orchestratorStrategy = strategy; + } catch (e) { + console.error("Erro na IA do Orchestrator:", e); + target.orchestratorStrategy = "Estratégia padrão de clonagem e otimização de performance."; + } + + // Step 1: Clone + updateOrchestratorStatus(`📥 [${i+1}/${targets.length}] Clonando: ${target.name}`); + let directory = `cloned-sites/${target.name.toLowerCase().replace(/[^a-z0-9]/g, '')}`; + try { + const cloneRes = await cloneSite(target.url); + if (cloneRes.success && (cloneRes.data?.directory || cloneRes.data?.path || cloneRes.directory)) { + directory = cloneRes.data?.directory || cloneRes.data?.path || cloneRes.directory; + } + } catch (e) { + console.warn(`Aviso ao clonar ${target.name}, usando diretório de fallback:`, e.message); + } + target.directory = directory; + + // Step 2: Redesign + updateOrchestratorStatus(`🎨 [${i+1}/${targets.length}] Aplicando Redesign AI: ${target.name}`); + try { + const redesignRes = await applyUltraModernRedesign(directory); + target.redesign = redesignRes; + } catch (e) { + console.error(`Erro no redesign de ${target.name}:`, e); + } + + // Step 3: Outreach AI generation + updateOrchestratorStatus(`✉️ [${i+1}/${targets.length}] Gerando Copy de Outreach: ${target.name}`); + const outreachPrompt = `Você é um Copywriter (Agente Outreach). Escreva uma mensagem de vendas persuasiva para '${target.name}' (${target.url}) focando em resolver '${target.reason}' com um redesign ultra-moderno e alta performance. Responda apenas o texto do e-mail.`; + try { + const message = await askAI(outreachPrompt, "Crie o e-mail de vendas."); + agentsState.outreach.messages.push({ + to: target.name, + toUrl: target.url, + message, + sentAt: new Date().toISOString(), + status: 'ready' + }); + agentsState.outreach.sent++; + updateOutreachStats(); + } catch (e) { + console.error(`Erro no outreach de ${target.name}:`, e); + } + + agentsState.orchestrator.currentStep = i + 1; + updateOrchestratorProgress((i + 1) / targets.length * 100); + } + + updateOrchestratorStatus(`✅ Pipeline completo! ${targets.length} sites processados com IA.`); + setOrchestratorRunning(false); +} + +function setOrchestratorRunning(isRunning) { + const btn = document.getElementById('runPipelineBtn'); + if (btn) { + btn.disabled = isRunning; + btn.innerHTML = isRunning + ? ' Executando...' + : ' Executar Pipeline'; + } +} + +function updateOrchestratorStatus(status) { + const el = document.getElementById('orchestratorStatus'); + if (el) el.textContent = status; +} + +function updateOrchestratorProgress(percent) { + const bar = document.getElementById('orchestratorProgress'); + if (bar) bar.style.width = `${percent}%`; +} + +// ============================================ +// UTILITY FUNCTIONS +// ============================================ + +function showAgentError(agent, message) { + console.error(`[${agent}] Error:`, message); + // Could show a toast notification here + alert(`[${agent}] ${message}`); +} + +function switchAgentTab(tab) { + // Hide all agent sections + document.querySelectorAll('.agent-section').forEach(el => el.style.display = 'none'); + + // Show selected tab content + const section = document.getElementById(`agent-${tab}`); + if (section) section.style.display = 'block'; + + // Update tab buttons + document.querySelectorAll('.agent-tab-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.tab === tab); + }); +} + +// Initialize agents when DOM is ready +document.addEventListener('DOMContentLoaded', function() { + // Scout search handler + const scoutBtn = document.getElementById('scoutSearchBtn'); + if (scoutBtn) { + scoutBtn.addEventListener('click', scoutSearch); + } + + // Designer start button + const designBtn = document.getElementById('startDesignBtn'); + if (designBtn) { + designBtn.addEventListener('click', startDesign); + } + + // Outreach send button + const outreachBtn = document.getElementById('outreachSendBtn'); + if (outreachBtn) { + outreachBtn.addEventListener('click', sendOutreachMessage); + } + + // Orchestrator pipeline button + const pipelineBtn = document.getElementById('runPipelineBtn'); + if (pipelineBtn) { + pipelineBtn.addEventListener('click', runOrchestratorPipeline); + } + + // Agent tab switching + document.querySelectorAll('.agent-tab-btn').forEach(btn => { + btn.addEventListener('click', () => switchAgentTab(btn.dataset.tab)); + }); + + console.log('✅ BrainSteel Web Agents initialized'); +}); diff --git a/web-interface/public/storage-panel.js b/web-interface/public/storage-panel.js new file mode 100644 index 0000000..9164eed --- /dev/null +++ b/web-interface/public/storage-panel.js @@ -0,0 +1,519 @@ +// ============================================ +// BRAINSTEEL WEB - STORAGE PANEL +// ============================================ + +let storageState = { + clones: [], + selectedClone: null, + viewMode: 'grid' // 'grid' or 'list' +}; + +let storageApi = { + // Fetch all clones from API + async fetchClones() { + const res = await fetch('/api/clones'); + const data = await res.json(); + return data.data; + }, + + // Get single clone details + async getClone(id) { + const res = await fetch(`/api/clones/${id}`); + const data = await res.json(); + return data.data; + }, + + // Update clone metadata + async updateClone(id, updates) { + const res = await fetch(`/api/clones/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates) + }); + return res.json(); + }, + + // Delete clone + async deleteClone(id) { + const res = await fetch(`/api/clones/${id}`, { method: 'DELETE' }); + return res.json(); + }, + + // Create version snapshot + async createVersion(id) { + const res = await fetch(`/api/clones/${id}/versions`, { method: 'POST' }); + return res.json(); + }, + + // Restore from version + async restoreVersion(id, versionId) { + const res = await fetch(`/api/clones/${id}/restore/${versionId}`, { method: 'POST' }); + return res.json(); + }, + + // Delete version + async deleteVersion(id, versionId) { + const res = await fetch(`/api/clones/${id}/versions/${versionId}`, { method: 'DELETE' }); + return res.json(); + } +}; + +// ============================================ +// UI FUNCTIONS +// ============================================ + +async function loadStoragePanel() { + const container = document.getElementById('storageContent'); + if (!container) return; + + showStorageLoading(true); + + try { + const data = await storageApi.fetchClones(); + storageState.clones = data.clones; + + renderStorageSummary(data.summary); + renderClonesGrid(data.clones); + showStorageLoading(false); + } catch (err) { + console.error('Erro ao carregar storage:', err); + container.innerHTML = `
+ +

Erro ao carregar clones: ${err.message}

+ +
`; + showStorageLoading(false); + } +} + +function showStorageLoading(show) { + const loader = document.getElementById('storageLoader'); + if (loader) loader.style.display = show ? 'flex' : 'none'; +} + +function renderStorageSummary(summary) { + const el = document.getElementById('storageSummary'); + if (!el || !summary) return; + + el.innerHTML = ` +
+ +
+

${summary.total}

+

Total de Clones

+
+
+
+ +
+

${summary.formattedSize || '0 MB'}

+

Espaço Usado

+
+
+
+ +
+

${summary.byStatus?.active || 0}

+

Ativos

+
+
+
+ +
+

${summary.byStatus?.archived || 0}

+

Arquivados

+
+
+ `; +} + +function renderClonesGrid(clones) { + const container = document.getElementById('storageClonesGrid'); + if (!container) return; + + if (!clones || clones.length === 0) { + container.innerHTML = ` +
+ +

Nenhum clone encontrado

+

Clone um site para começar a gerenciar seu storage

+
`; + return; + } + + container.innerHTML = clones.map(clone => ` +
+
+
+
+ +
+
+
+
+ +
+

${truncateUrl(clone.baseUrl)}

+

${clone.baseUrl}

+
+ ${formatDate(clone.cloneInfo?.clonedAt)} + ${clone.stats?.fileCount || 0} arquivos + ${clone.stats?.formattedSize || '0 B'} +
+
+ +
+ `).join(''); +} + +function showCloneDetails(cloneId) { + const clone = storageState.clones.find(c => c.id === cloneId); + if (!clone) return; + + storageState.selectedClone = clone; + + const modal = createModal('cloneDetailsModal', ` + + + + `); + + document.body.appendChild(modal); + showModal('cloneDetailsModal'); +} + +function renderVersionsList(versions) { + if (!versions || versions.length === 0) { + return '

Nenhuma versão salva. Clique em "Criar Snapshot" para salvar o estado atual.

'; + } + + return versions.map(v => ` +
+
+ + ${v.id} + ${formatDate(v.createdAt)} + ${formatBytes(v.size)} +
+
+ + +
+
+ `).join(''); +} + +function showCloneVersions(cloneId) { + const clone = storageState.clones.find(c => c.id === cloneId); + if (clone) { + storageState.selectedClone = clone; + showCloneDetails(cloneId); + } +} + +// ============================================ +// ACTIONS +// ============================================ + +async function updateCloneStatus(cloneId, status) { + try { + await storageApi.updateClone(cloneId, { status }); + showToast('Status atualizado', 'success'); + loadStoragePanel(); + } catch (err) { + showToast('Erro ao atualizar status', 'error'); + } +} + +async function saveCloneTags(cloneId, tagsString) { + const tags = tagsString.split(',').map(t => t.trim()).filter(t => t); + try { + await storageApi.updateClone(cloneId, { tags }); + showToast('Tags salvas', 'success'); + } catch (err) { + showToast('Erro ao salvar tags', 'error'); + } +} + +async function createNewVersion(cloneId) { + try { + showToast('Criando snapshot...', 'info'); + const result = await storageApi.createVersion(cloneId); + if (result.success) { + showToast('Snapshot criado com sucesso', 'success'); + // Refresh the details modal + showCloneDetails(cloneId); + loadStoragePanel(); + } + } catch (err) { + showToast('Erro ao criar snapshot: ' + err.message, 'error'); + } +} + +async function restoreCloneVersion(cloneId, versionId) { + if (!confirm('Restaurar esta versão? O estado atual será salvo como backup.')) return; + + try { + showToast('Restaurando versão...', 'info'); + const result = await storageApi.restoreVersion(cloneId, versionId); + if (result.success) { + showToast('Versão restaurada com sucesso', 'success'); + closeModal('cloneDetailsModal'); + loadStoragePanel(); + } + } catch (err) { + showToast('Erro ao restaurar: ' + err.message, 'error'); + } +} + +async function deleteCloneVersion(cloneId, versionId) { + if (!confirm('Excluir esta versão?')) return; + + try { + const result = await storageApi.deleteVersion(cloneId, versionId); + if (result.success) { + showToast('Versão removida', 'success'); + showCloneDetails(cloneId); + loadStoragePanel(); + } + } catch (err) { + showToast('Erro ao remover versão', 'error'); + } +} + +function confirmDeleteClone(cloneId) { + if (!confirm('Tem certeza que deseja excluir este clone permanentemente?')) return; + + storageApi.deleteClone(cloneId) + .then(result => { + if (result.success) { + showToast('Clone excluído', 'success'); + closeModal('cloneDetailsModal'); + loadStoragePanel(); + } + }) + .catch(err => showToast('Erro ao excluir: ' + err.message, 'error')); +} + +function openClone(cloneId) { + window.open(`/${cloneId}/index.html`, '_blank'); +} + +function cloneSiteAgain(baseUrl) { + // Navigate to new clone page with URL pre-filled + switchAgentTab('designer'); + const urlInput = document.getElementById('designUrl'); + if (urlInput) { + urlInput.value = baseUrl.startsWith('http') ? baseUrl : 'https://' + baseUrl; + } + showToast(`URL carregada: ${baseUrl}`, 'info'); +} + +function showCloneMenu(cloneId) { + // Simple context menu + const clone = storageState.clones.find(c => c.id === cloneId); + if (!clone) return; + + const action = prompt(`Ações para ${clone.baseUrl}: +1. Abrir +2. Detalhes +3. Versões +4. Reclonar +5. Excluir + +Digite o número:`); + + if (action === '1') openClone(cloneId); + else if (action === '2') showCloneDetails(cloneId); + else if (action === '3') showCloneVersions(cloneId); + else if (action === '4') cloneSiteAgain(clone.baseUrl); + else if (action === '5') confirmDeleteClone(cloneId); +} + +// ============================================ +// MODAL HELPERS +// ============================================ + +function createModal(id, content) { + const existing = document.getElementById(id); + if (existing) existing.remove(); + + const modal = document.createElement('div'); + modal.id = id; + modal.className = 'modal'; + modal.innerHTML = ``; + + modal.addEventListener('click', (e) => { + if (e.target.classList.contains('modal')) closeModal(id); + }); + + return modal; +} + +function showModal(id) { + const modal = document.getElementById(id); + if (modal) { + modal.style.display = 'flex'; + setTimeout(() => modal.classList.add('active'), 10); + } +} + +function closeModal(id) { + const modal = document.getElementById(id); + if (modal) { + modal.classList.remove('active'); + setTimeout(() => modal.remove(), 300); + } +} + +// ============================================ +// UTILITIES +// ============================================ + +function truncateUrl(url, maxLen = 30) { + if (!url) return ''; + if (url.length <= maxLen) return url; + return url.substring(0, maxLen) + '...'; +} + +function formatDate(dateStr) { + if (!dateStr) return 'N/A'; + const date = new Date(dateStr); + return date.toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +} + +function formatBytes(bytes) { + if (!bytes || bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +function showToast(message, type = 'info') { + // Simple alert for now - could be replaced with a proper toast system + console.log(`[${type}] ${message}`); + // Create a simple toast element + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + toast.textContent = message; + toast.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 24px; + background: ${type === 'success' ? '#43e97b' : type === 'error' ? '#f5576c' : '#667eea'}; + color: white; + border-radius: 8px; + z-index: 10000; + animation: slideIn 0.3s ease; + `; + document.body.appendChild(toast); + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => toast.remove(), 300); + }, 3000); +} + +// ============================================ +// INIT +// ============================================ + +document.addEventListener('DOMContentLoaded', function() { + // If storage page element exists, load the panel + const storageContent = document.getElementById('storageContent'); + if (storageContent) { + loadStoragePanel(); + } + + // Add storage nav click handler + const storageNav = document.querySelector('[data-page="storage"]'); + if (storageNav) { + storageNav.addEventListener('click', loadStoragePanel); + } + + console.log('✅ Storage Panel initialized'); +}); diff --git a/web-interface/server.js b/web-interface/server.js index 9aeb856..6b20fe4 100644 --- a/web-interface/server.js +++ b/web-interface/server.js @@ -77,12 +77,13 @@ app.post('/api/wget-clone', async (req, res) => { console.log('✅ Wget clone completed successfully'); // Sync to host /root/Desktop/Clonados - if (response.data && response.data.success && response.data.directory) { + const directory = response.data.data?.directory || response.data.directory; + if (response.data && response.data.success && directory) { const { exec } = require('child_process'); const containerId = require('os').hostname(); - const cloneDir = response.data.directory.split('/').pop(); + const cloneDir = directory.split('/').pop(); const hostDir = '/root/Desktop/Clonados'; - exec(`docker cp b38l2zsxjv08xkxg4gd5rl9r-210808689309:${response.data.directory} ${hostDir}/ 2>/dev/null`, (err) => { + exec(`docker cp ${containerId}:${directory} ${hostDir}/ 2>/dev/null`, (err) => { if (err) console.log('⚠️ Sync to host failed:', err.message); else console.log('✅ Clone synced to host:', cloneDir); }); @@ -281,6 +282,42 @@ app.get('/index', (req, res) => { res.redirect('/'); }); +// AI Agent Route (MiniMax) +app.post('/api/ai/agent', async (req, res) => { + const { systemPrompt, userMessage } = req.body; + const apiKey = process.env.MINIMAX_API_KEY; + + if (!apiKey) { + return res.status(500).json({ error: 'MINIMAX_API_KEY is missing in .env' }); + } + + try { + const response = await axios.post('https://api.minimax.io/v1/chat/completions', { + model: "MiniMax-M2.7", + messages: [ + { role: "system", content: systemPrompt || "Você é um assistente de inteligência artificial útil." }, + { role: "user", content: userMessage } + ], + max_tokens: 2048 + }, { + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + timeout: 90000 // 90s + }); + + let reply = response.data.choices?.[0]?.message?.content || 'Processamento concluído, porém sem retorno.'; + // Strip reasoning tags if present + reply = reply.replace(/[\s\S]*?<\/think>/gi, '').trim(); + + res.json({ success: true, data: reply }); + } catch (error) { + console.error('MiniMax AI Error:', error.response?.data || error.message); + res.status(500).json({ error: error.message, details: error.response?.data }); + } +}); + // Handle 404s app.use((req, res) => { res.status(404).json({ diff --git a/web-interface/storage-manager.js b/web-interface/storage-manager.js new file mode 100644 index 0000000..686ff81 --- /dev/null +++ b/web-interface/storage-manager.js @@ -0,0 +1,421 @@ +/** + * CloneWeb Storage Manager + * Gerencia versões, metadados e storage dos sites clonados + */ + +const fs = require('fs'); +const path = require('path'); +const express = require('express'); +const router = express.Router(); + +const BASE_DIR = '/app/cloned-sites'; +const METADATA_FILE = 'clone-metadata.json'; + +/** + * Get all clones with their metadata + */ +function getAllClones() { + const clones = []; + + if (!fs.existsSync(BASE_DIR)) { + return clones; + } + + const dirs = fs.readdirSync(BASE_DIR).filter(f => { + return fs.statSync(path.join(BASE_DIR, f)).isDirectory(); + }); + + for (const dir of dirs) { + const clonePath = path.join(BASE_DIR, dir); + const metadata = loadMetadata(clonePath); + const cloneInfo = loadCloneInfo(clonePath); + const stats = getCloneStats(clonePath); + + // Extract base URL from directory name + const baseUrl = extractBaseUrl(dir); + + clones.push({ + id: dir, + directory: dir, + baseUrl, + path: clonePath, + metadata: metadata || {}, + cloneInfo: cloneInfo || {}, + stats, + versions: listVersions(clonePath) + }); + } + + // Sort by most recent + clones.sort((a, b) => { + const dateA = new Date(a.cloneInfo.clonedAt || a.metadata.createdAt || 0); + const dateB = new Date(b.cloneInfo.clonedAt || b.metadata.createdAt || 0); + return dateB - dateA; + }); + + return clones; +} + +function loadMetadata(clonePath) { + const metaFile = path.join(clonePath, METADATA_FILE); + if (fs.existsSync(metaFile)) { + try { + return JSON.parse(fs.readFileSync(metaFile, 'utf8')); + } catch (e) { + return null; + } + } + return null; +} + +function loadCloneInfo(clonePath) { + const infoFile = path.join(clonePath, 'clone-info.json'); + if (fs.existsSync(infoFile)) { + try { + return JSON.parse(fs.readFileSync(infoFile, 'utf8')); + } catch (e) { + return null; + } + } + return null; +} + +function getCloneStats(clonePath) { + let totalSize = 0; + let fileCount = 0; + + function countFiles(dir) { + if (!fs.existsSync(dir)) return; + + const items = fs.readdirSync(dir); + for (const item of items) { + const itemPath = path.join(dir, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory()) { + // Skip cloned-sites subdirectory + if (!item.includes('cloned-sites')) { + countFiles(itemPath); + } + } else { + totalSize += stat.size; + fileCount++; + } + } + } + + countFiles(clonePath); + + return { + totalSize, + fileCount, + formattedSize: formatBytes(totalSize) + }; +} + +function listVersions(clonePath) { + const versions = []; + const versionsDir = path.join(clonePath, '.versions'); + + if (fs.existsSync(versionsDir)) { + const versionDirs = fs.readdirSync(versionsDir); + for (const vDir of versionDirs) { + const vPath = path.join(versionsDir, vDir); + const stat = fs.statSync(vPath); + versions.push({ + id: vDir, + path: vPath, + createdAt: stat.birthtime, + size: getDirSize(vPath) + }); + } + } + + return versions; +} + +function getDirSize(dirPath) { + let size = 0; + function count(dir) { + if (!fs.existsSync(dir)) return; + for (const item of fs.readdirSync(dir)) { + const stat = fs.statSync(path.join(dir, item)); + if (stat.isDirectory()) { + count(path.join(dir, item)); + } else { + size += stat.size; + } + } + } + count(dirPath); + return size; +} + +function extractBaseUrl(dirName) { + // "example.com_2026-05-17T20-58-30-760Z" -> "example.com" + const parts = dirName.split('_'); + if (parts.length >= 2) { + // Check if last part is a timestamp + const lastPart = parts[parts.length - 1]; + if (/^\d{4}-\d{2}-\d{2}T/.test(lastPart)) { + return parts.slice(0, -1).join('_'); + } + } + return parts[0]; +} + +function formatBytes(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +// ============================================ +// API ROUTES +// ============================================ + +// GET /api/clones - List all clones +router.get('/', (req, res) => { + try { + const clones = getAllClones(); + + const summary = { + total: clones.length, + totalSize: clones.reduce((sum, c) => sum + c.stats.totalSize, 0), + formattedSize: formatBytes(clones.reduce((sum, c) => sum + c.stats.totalSize, 0)), + byStatus: { + active: clones.filter(c => c.metadata?.status === 'active').length, + archived: clones.filter(c => c.metadata?.status === 'archived').length, + redesign: clones.filter(c => c.metadata?.status === 'redesign').length + } + }; + + res.json({ + success: true, + data: { + clones, + summary + } + }); + } catch (error) { + console.error('Erro ao listar clones:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// GET /api/clones/:id - Get single clone details +router.get('/:id', (req, res) => { + try { + const clonePath = path.join(BASE_DIR, req.params.id); + + if (!fs.existsSync(clonePath)) { + return res.status(404).json({ success: false, error: 'Clone não encontrado' }); + } + + const metadata = loadMetadata(clonePath); + const cloneInfo = loadCloneInfo(clonePath); + const stats = getCloneStats(clonePath); + const versions = listVersions(clonePath); + + // Get list of HTML files + const htmlFiles = findFiles(clonePath, '.html'); + + res.json({ + success: true, + data: { + id: req.params.id, + baseUrl: extractBaseUrl(req.params.id), + path: clonePath, + metadata: metadata || { createdAt: cloneInfo?.clonedAt }, + cloneInfo, + stats, + versions, + htmlFiles + } + }); + } catch (error) { + console.error('Erro ao buscar clone:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// PATCH /api/clones/:id - Update clone metadata +router.patch('/:id', (req, res) => { + try { + const clonePath = path.join(BASE_DIR, req.params.id); + + if (!fs.existsSync(clonePath)) { + return res.status(404).json({ success: false, error: 'Clone não encontrado' }); + } + + const metaFile = path.join(clonePath, METADATA_FILE); + let metadata = loadMetadata(clonePath) || {}; + + // Merge updates + metadata = { + ...metadata, + ...req.body, + updatedAt: new Date().toISOString() + }; + + fs.writeFileSync(metaFile, JSON.stringify(metadata, null, 2)); + + res.json({ success: true, data: { metadata } }); + } catch (error) { + console.error('Erro ao atualizar clone:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// POST /api/clones/:id/versions - Create a version snapshot +router.post('/:id/versions', (req, res) => { + try { + const clonePath = path.join(BASE_DIR, req.params.id); + + if (!fs.existsSync(clonePath)) { + return res.status(404).json({ success: false, error: 'Clone não encontrado' }); + } + + const versionsDir = path.join(clonePath, '.versions'); + const versionId = `v${Date.now()}`; + const versionPath = path.join(versionsDir, versionId); + + fs.mkdirSync(versionPath, { recursive: true }); + + // Copy all files to version (except .versions itself) + copyDirectory(clonePath, versionPath, ['.versions']); + + res.json({ + success: true, + data: { + versionId, + path: versionPath, + createdAt: new Date().toISOString() + } + }); + } catch (error) { + console.error('Erro ao criar versão:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// DELETE /api/clones/:id/versions/:versionId - Delete a version +router.delete('/:id/versions/:versionId', (req, res) => { + try { + const versionPath = path.join(BASE_DIR, req.params.id, '.versions', req.params.versionId); + + if (!fs.existsSync(versionPath)) { + return res.status(404).json({ success: false, error: 'Versão não encontrada' }); + } + + fs.rmSync(versionPath, { recursive: true, force: true }); + + res.json({ success: true, message: 'Versão removida' }); + } catch (error) { + console.error('Erro ao remover versão:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// POST /api/clones/:id/restore/:versionId - Restore from version +router.post('/:id/restore/:versionId', (req, res) => { + try { + const clonePath = path.join(BASE_DIR, req.params.id); + const versionPath = path.join(clonePath, '.versions', req.params.versionId); + + if (!fs.existsSync(versionPath)) { + return res.status(404).json({ success: false, error: 'Versão não encontrada' }); + } + + // Backup current state first + const backupId = `pre-restore-${Date.now()}`; + const backupPath = path.join(clonePath, '.versions', backupId); + fs.mkdirSync(backupPath, { recursive: true }); + copyDirectory(clonePath, backupPath, ['.versions']); + + // Create restore point + const restorePointId = `pre-restore-${Date.now()}`; + const restorePointPath = path.join(clonePath, '.versions', restorePointId); + fs.mkdirSync(restorePointPath, { recursive: true }); + copyDirectory(clonePath, restorePointPath, ['.versions']); + + // Copy version files back (overwrite) + copyDirectory(versionPath, clonePath, ['.versions'], true); + + res.json({ + success: true, + data: { + restoredFrom: req.params.versionId, + backupCreated: restorePointId + } + }); + } catch (error) { + console.error('Erro ao restaurar versão:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// DELETE /api/clones/:id - Delete a clone +router.delete('/:id', (req, res) => { + try { + const clonePath = path.join(BASE_DIR, req.params.id); + + if (!fs.existsSync(clonePath)) { + return res.status(404).json({ success: false, error: 'Clone não encontrado' }); + } + + fs.rmSync(clonePath, { recursive: true, force: true }); + + res.json({ success: true, message: 'Clone removido' }); + } catch (error) { + console.error('Erro ao remover clone:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// Helper functions +function findFiles(dir, extension) { + const files = []; + + function search(d) { + if (!fs.existsSync(d)) return; + for (const item of fs.readdirSync(d)) { + const itemPath = path.join(d, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory() && item !== '.versions' && !item.includes('node_modules')) { + search(itemPath); + } else if (item.endsWith(extension)) { + files.push(path.relative(dir, itemPath)); + } + } + } + + search(dir); + return files; +} + +function copyDirectory(src, dest, exclude = [], overwrite = true) { + if (!fs.existsSync(src)) return; + fs.mkdirSync(dest, { recursive: true }); + + for (const item of fs.readdirSync(src)) { + if (exclude.includes(item)) continue; + + const srcPath = path.join(src, item); + const destPath = path.join(dest, item); + const stat = fs.statSync(srcPath); + + if (stat.isDirectory()) { + copyDirectory(srcPath, destPath, exclude, overwrite); + } else { + if (overwrite || !fs.existsSync(destPath)) { + fs.copyFileSync(srcPath, destPath); + } + } + } +} + +module.exports = router;