Atualização automática: 2026-05-18 11:16:56

This commit is contained in:
Hermes
2026-05-18 11:16:56 +00:00
parent 7e6e105fdb
commit 96d1684849
4 changed files with 1500 additions and 3 deletions
+520
View File
@@ -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 = '<p class="text-muted">Nenhum candidato encontrado</p>';
return;
}
const html = targets.map(t => `
<div class="scout-target-card" data-id="${t.id}">
<div class="target-header">
<span class="target-score score-${t.score >= 8 ? 'high' : t.score >= 6 ? 'medium' : 'low'}">${t.score}</span>
<div class="target-info">
<h4>${t.name}</h4>
<a href="${t.url}" target="_blank" class="target-url">${t.url}</a>
</div>
</div>
<div class="target-body">
<p class="target-reason">${t.reason}</p>
<div class="target-meta">
<span><i class="fab fa-instagram"></i> ${t.social}</span>
<span class="target-niche">${t.niche}</span>
</div>
</div>
<div class="target-actions">
<button class="btn btn-sm btn-primary" onclick="selectTargetForDesign(${t.id})">
<i class="fas fa-magic"></i> Redesign
</button>
<button class="btn btn-sm btn-secondary" onclick="openTargetURL(${t.id})">
<i class="fas fa-external-link-alt"></i> Ver Site
</button>
</div>
</div>
`).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
? '<i class="fas fa-spinner fa-spin"></i> Pesquisando...'
: '<i class="fas fa-search"></i> 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
? '<i class="fas fa-spinner fa-spin"></i> Processando...'
: '<i class="fas fa-magic"></i> 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
? '<i class="fas fa-spinner fa-spin"></i> Executando...'
: '<i class="fas fa-play"></i> 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');
});
+519
View File
@@ -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 = `<div class="storage-error">
<i class="fas fa-exclamation-triangle"></i>
<p>Erro ao carregar clones: ${err.message}</p>
<button class="btn btn-secondary" onclick="loadStoragePanel()">
<i class="fas fa-redo"></i> Tentar novamente
</button>
</div>`;
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 = `
<div class="summary-card">
<i class="fas fa-globe"></i>
<div>
<h4>${summary.total}</h4>
<p>Total de Clones</p>
</div>
</div>
<div class="summary-card">
<i class="fas fa-hdd"></i>
<div>
<h4>${summary.formattedSize || '0 MB'}</h4>
<p>Espaço Usado</p>
</div>
</div>
<div class="summary-card">
<i class="fas fa-check-circle"></i>
<div>
<h4>${summary.byStatus?.active || 0}</h4>
<p>Ativos</p>
</div>
</div>
<div class="summary-card">
<i class="fas fa-archive"></i>
<div>
<h4>${summary.byStatus?.archived || 0}</h4>
<p>Arquivados</p>
</div>
</div>
`;
}
function renderClonesGrid(clones) {
const container = document.getElementById('storageClonesGrid');
if (!container) return;
if (!clones || clones.length === 0) {
container.innerHTML = `
<div class="storage-empty">
<i class="fas fa-folder-open"></i>
<h3>Nenhum clone encontrado</h3>
<p>Clone um site para começar a gerenciar seu storage</p>
</div>`;
return;
}
container.innerHTML = clones.map(clone => `
<div class="clone-card" data-id="${clone.id}">
<div class="clone-card-header">
<div class="clone-status ${clone.metadata?.status || 'active'}"></div>
<div class="clone-card-actions">
<button class="btn-icon" onclick="event.stopPropagation(); showCloneMenu('${clone.id}')" title="Menu">
<i class="fas fa-ellipsis-v"></i>
</button>
</div>
</div>
<div class="clone-card-body" onclick="showCloneDetails('${clone.id}')">
<div class="clone-icon">
<i class="fas fa-globe"></i>
</div>
<h4 class="clone-name" title="${clone.baseUrl}">${truncateUrl(clone.baseUrl)}</h4>
<p class="clone-url">${clone.baseUrl}</p>
<div class="clone-meta">
<span><i class="fas fa-clock"></i> ${formatDate(clone.cloneInfo?.clonedAt)}</span>
<span><i class="fas fa-file"></i> ${clone.stats?.fileCount || 0} arquivos</span>
<span><i class="fas fa-hdd"></i> ${clone.stats?.formattedSize || '0 B'}</span>
</div>
</div>
<div class="clone-card-footer">
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); openClone('${clone.id}')">
<i class="fas fa-external-link-alt"></i> Abrir
</button>
<button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); showCloneVersions('${clone.id}')">
<i class="fas fa-history"></i> Versões
</button>
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); cloneSiteAgain('${clone.baseUrl}')">
<i class="fas fa-redo"></i> Reclonar
</button>
</div>
</div>
`).join('');
}
function showCloneDetails(cloneId) {
const clone = storageState.clones.find(c => c.id === cloneId);
if (!clone) return;
storageState.selectedClone = clone;
const modal = createModal('cloneDetailsModal', `
<div class="modal-header">
<h3><i class="fas fa-globe"></i> ${clone.baseUrl}</h3>
<button class="modal-close" onclick="closeModal('cloneDetailsModal')">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="clone-details-grid">
<div class="detail-item">
<label>URL Original</label>
<a href="${clone.cloneInfo?.originalUrl || '#'}" target="_blank">${clone.cloneInfo?.originalUrl || 'N/A'}</a>
</div>
<div class="detail-item">
<label>Data da Clonagem</label>
<span>${formatDate(clone.cloneInfo?.clonedAt)}</span>
</div>
<div class="detail-item">
<label>Tamanho</label>
<span>${clone.stats?.formattedSize || '0 B'}</span>
</div>
<div class="detail-item">
<label>Arquivos</label>
<span>${clone.stats?.fileCount || 0}</span>
</div>
<div class="detail-item">
<label>Status</label>
<select onchange="updateCloneStatus('${clone.id}', this.value)">
<option value="active" ${(clone.metadata?.status) === 'active' ? 'selected' : ''}>Ativo</option>
<option value="archived" ${(clone.metadata?.status) === 'archived' ? 'selected' : ''}>Arquivado</option>
<option value="redesign" ${(clone.metadata?.status) === 'redesign' ? 'selected' : ''}>Em Redesign</option>
</select>
</div>
<div class="detail-item">
<label>Tags</label>
<input type="text" id="cloneTagsInput" value="${clone.metadata?.tags?.join(', ') || ''}"
placeholder="Separar por vírgulas" onblur="saveCloneTags('${clone.id}', this.value)">
</div>
</div>
<div class="clone-versions-section">
<h4><i class="fas fa-history"></i> Versões Salvas</h4>
<div id="cloneVersionsList" class="versions-list">
${renderVersionsList(clone.versions)}
</div>
<button class="btn btn-secondary" onclick="createNewVersion('${clone.id}')">
<i class="fas fa-camera"></i> Criar Snapshot
</button>
</div>
<div class="clone-html-files">
<h4><i class="fas fa-file-code"></i> Arquivos HTML</h4>
<ul>
${(clone.htmlFiles || []).map(f => `<li><a href="/${clone.id}/${f}" target="_blank">${f}</a></li>`).join('')}
</ul>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-danger" onclick="confirmDeleteClone('${clone.id}')">
<i class="fas fa-trash"></i> Excluir Clone
</button>
<button class="btn btn-secondary" onclick="closeModal('cloneDetailsModal')">Fechar</button>
</div>
`);
document.body.appendChild(modal);
showModal('cloneDetailsModal');
}
function renderVersionsList(versions) {
if (!versions || versions.length === 0) {
return '<p class="text-muted">Nenhuma versão salva. Clique em "Criar Snapshot" para salvar o estado atual.</p>';
}
return versions.map(v => `
<div class="version-item">
<div class="version-info">
<i class="fas fa-archive"></i>
<span>${v.id}</span>
<span class="version-date">${formatDate(v.createdAt)}</span>
<span class="version-size">${formatBytes(v.size)}</span>
</div>
<div class="version-actions">
<button class="btn btn-sm btn-secondary" onclick="restoreCloneVersion('${storageState.selectedClone?.id}', '${v.id}')">
<i class="fas fa-undo"></i> Restaurar
</button>
<button class="btn btn-sm btn-danger" onclick="deleteCloneVersion('${storageState.selectedClone?.id}', '${v.id}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).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 = `<div class="modal-content">${content}</div>`;
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');
});
+40 -3
View File
@@ -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 <think> reasoning tags if present
reply = reply.replace(/<think>[\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({
+421
View File
@@ -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;