Last active
January 26, 2026 13:16
-
-
Save camillanapoles/30d4066eb7bb37f8ece19b661b7ba24e to your computer and use it in GitHub Desktop.
Claude Extractor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Claude Extractor Diagnostic | |
| // @namespace https://github.com/cnmfs/claude-extractor | |
| // @version 2.1.0-diag | |
| // @description Versão de diagnóstico para identificar seletores DOM do Claude.ai | |
| // @author cnmfs | |
| // @match https://claude.ai/* | |
| // @icon https://claude.ai/favicon.ico | |
| // @grant none | |
| // @run-at document-idle | |
| // @license MIT | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| const VERSION = '2.1.0-diag'; | |
| // ═══════════════════════════════════════════════════════════ | |
| // SELETORES A TESTAR | |
| // ═══════════════════════════════════════════════════════════ | |
| const SELECTORS_TO_TEST = { | |
| // CONTAINER - Onde estão as mensagens | |
| container: [ | |
| '[data-testid="conversation-turn-list"]', | |
| '[data-testid="conversation-messages"]', | |
| '[role="main"]', | |
| 'main [class*="flex-col"]', | |
| '[class*="react-scroll-to-bottom"]', | |
| '[class*="overflow-y-auto"]', | |
| 'main > div > div', | |
| 'main' | |
| ], | |
| // MESSAGE GROUP - Cada mensagem individual | |
| messageGroup: [ | |
| '[data-testid^="conversation-turn"]', | |
| '[data-testid*="turn"]', | |
| '[class*="group/conversation-turn"]', | |
| '[class*="conversation-turn"]', | |
| 'div[class*="group"][class*="relative"]', | |
| 'div[class*="min-h-"]:has([class*="prose"])', | |
| 'div.group:has([class*="prose"])', | |
| '[role="article"]' | |
| ], | |
| // HUMAN - Mensagem do usuário | |
| human: [ | |
| '[data-testid="user-message"]', | |
| '[data-testid="human-turn"]', | |
| '[data-testid*="human"]', | |
| '[data-testid*="user"]', | |
| '[aria-label*="you"]', | |
| '[class*="human"]', | |
| '[class*="user-message"]' | |
| ], | |
| // ASSISTANT - Mensagem do Claude | |
| assistant: [ | |
| '[data-testid="assistant-message"]', | |
| '[data-testid="assistant-turn"]', | |
| '[data-testid*="assistant"]', | |
| '[aria-label*="Claude"]', | |
| '[class*="assistant"]', | |
| '[class*="ai-message"]' | |
| ], | |
| // CONTENT - Conteúdo da mensagem | |
| content: [ | |
| '[class*="prose"]', | |
| '[class*="markdown"]', | |
| '[class*="font-claude-message"]', | |
| '[class*="message-content"]', | |
| '.whitespace-pre-wrap', | |
| '[data-testid*="content"]' | |
| ], | |
| // THINKING - Blocos de pensamento | |
| thinking: [ | |
| '[data-testid="thinking-block"]', | |
| '[data-testid="thinking-summary"]', | |
| '[data-testid="extended-thinking"]', | |
| '[data-testid*="thinking"]', | |
| 'details[class*="thinking"]', | |
| '[class*="thinking-container"]', | |
| '[class*="thinking"]', | |
| 'details:has(summary)' | |
| ], | |
| // ARTIFACT - Código/artefatos | |
| artifact: [ | |
| '[data-testid="artifact"]', | |
| '[data-artifact-id]', | |
| '[class*="artifact-container"]', | |
| '[class*="artifact"]', | |
| 'pre:has(code)', | |
| 'pre code' | |
| ], | |
| // TOOLS - Chamadas de ferramenta | |
| tools: [ | |
| '[data-testid="tool-use"]', | |
| '[data-testid="tool-call"]', | |
| '[data-testid*="tool"]', | |
| '[class*="tool-use"]', | |
| '[class*="function-call"]' | |
| ], | |
| // CONSOLE - Output de código | |
| console: [ | |
| '[data-testid="code-output"]', | |
| '[data-testid="execution-result"]', | |
| '[class*="code-output"]', | |
| '[class*="execution-result"]', | |
| '[class*="console-output"]', | |
| 'pre[class*="output"]', | |
| '[class*="stdout"]' | |
| ] | |
| }; | |
| // ═══════════════════════════════════════════════════════════ | |
| // DIAGNÓSTICO | |
| // ═══════════════════════════════════════════════════════════ | |
| const Diagnostic = { | |
| run() { | |
| console.log('═══════════════════════════════════════════════'); | |
| console.log('🔍 CLAUDE EXTRACTOR DIAGNOSTIC v' + VERSION); | |
| console.log('═══════════════════════════════════════════════'); | |
| console.log('URL:', location.href); | |
| console.log('Timestamp:', new Date().toISOString()); | |
| console.log(''); | |
| const report = { | |
| version: VERSION, | |
| url: location.href, | |
| timestamp: new Date().toISOString(), | |
| results: {}, | |
| workingSelectors: {}, | |
| recommendations: [] | |
| }; | |
| // Testar cada categoria de seletores | |
| for (const [category, selectors] of Object.entries(SELECTORS_TO_TEST)) { | |
| console.log(`\n📋 Testando: ${category.toUpperCase()}`); | |
| report.results[category] = []; | |
| report.workingSelectors[category] = []; | |
| for (const sel of selectors) { | |
| try { | |
| const elements = document.querySelectorAll(sel); | |
| const count = elements.length; | |
| const status = count > 0 ? '✅' : '❌'; | |
| console.log(` ${status} ${sel} → ${count} encontrado(s)`); | |
| report.results[category].push({ | |
| selector: sel, | |
| count, | |
| status: count > 0 ? 'found' : 'not_found' | |
| }); | |
| if (count > 0) { | |
| report.workingSelectors[category].push(sel); | |
| // Mostrar sample do primeiro elemento | |
| const sample = elements[0]; | |
| const info = { | |
| tagName: sample.tagName, | |
| id: sample.id || '(none)', | |
| className: (sample.className || '').substring(0, 100), | |
| dataTestId: sample.getAttribute('data-testid') || '(none)', | |
| textLength: (sample.textContent || '').length | |
| }; | |
| console.log(` Sample:`, info); | |
| } | |
| } catch (e) { | |
| console.log(` ⚠️ ${sel} → ERRO: ${e.message}`); | |
| report.results[category].push({ | |
| selector: sel, | |
| error: e.message, | |
| status: 'error' | |
| }); | |
| } | |
| } | |
| } | |
| // Análise especial: encontrar todas as estruturas de mensagem | |
| console.log('\n═══════════════════════════════════════════════'); | |
| console.log('🔬 ANÁLISE PROFUNDA DA ESTRUTURA'); | |
| console.log('═══════════════════════════════════════════════'); | |
| this.deepAnalysis(report); | |
| // Gerar recomendações | |
| this.generateRecommendations(report); | |
| // Mostrar resumo | |
| console.log('\n═══════════════════════════════════════════════'); | |
| console.log('📊 RESUMO'); | |
| console.log('═══════════════════════════════════════════════'); | |
| for (const [cat, sels] of Object.entries(report.workingSelectors)) { | |
| if (sels.length > 0) { | |
| console.log(`✅ ${cat}: ${sels[0]}`); | |
| } else { | |
| console.log(`❌ ${cat}: NENHUM SELETOR FUNCIONA`); | |
| } | |
| } | |
| console.log('\n📝 RECOMENDAÇÕES:'); | |
| report.recommendations.forEach((r, i) => { | |
| console.log(`${i+1}. ${r}`); | |
| }); | |
| // Salvar report | |
| console.log('\n💾 Report salvo em: window.CE_DIAGNOSTIC_REPORT'); | |
| window.CE_DIAGNOSTIC_REPORT = report; | |
| // Criar botão para copiar | |
| this.createCopyButton(report); | |
| return report; | |
| }, | |
| deepAnalysis(report) { | |
| // Encontrar main | |
| const main = document.querySelector('main'); | |
| if (!main) { | |
| console.log('❌ Elemento <main> não encontrado!'); | |
| report.recommendations.push('Página pode não estar totalmente carregada'); | |
| return; | |
| } | |
| console.log('✅ <main> encontrado'); | |
| // Buscar divs com data-testid | |
| const testIdDivs = main.querySelectorAll('[data-testid]'); | |
| console.log(`\n📍 Elementos com data-testid: ${testIdDivs.length}`); | |
| const testIds = new Set(); | |
| testIdDivs.forEach(el => { | |
| const tid = el.getAttribute('data-testid'); | |
| testIds.add(tid); | |
| }); | |
| console.log('Unique data-testids:', Array.from(testIds).sort().join(', ')); | |
| report.foundTestIds = Array.from(testIds); | |
| // Buscar divs com role | |
| const roleDivs = main.querySelectorAll('[role]'); | |
| console.log(`\n📍 Elementos com role: ${roleDivs.length}`); | |
| const roles = new Set(); | |
| roleDivs.forEach(el => { | |
| roles.add(el.getAttribute('role')); | |
| }); | |
| console.log('Unique roles:', Array.from(roles).sort().join(', ')); | |
| report.foundRoles = Array.from(roles); | |
| // Buscar classes comuns | |
| console.log('\n📍 Classes frequentes:'); | |
| const classCount = {}; | |
| main.querySelectorAll('*').forEach(el => { | |
| (el.className || '').split(/\s+/).forEach(c => { | |
| if (c && c.length > 3) { | |
| // Normalizar classes hashadas | |
| const normalized = c.replace(/[-_][a-z0-9]{5,}$/i, ''); | |
| classCount[normalized] = (classCount[normalized] || 0) + 1; | |
| } | |
| }); | |
| }); | |
| const topClasses = Object.entries(classCount) | |
| .filter(([c, n]) => n > 5) | |
| .sort((a, b) => b[1] - a[1]) | |
| .slice(0, 30); | |
| topClasses.forEach(([c, n]) => { | |
| console.log(` ${c}: ${n}x`); | |
| }); | |
| report.topClasses = topClasses; | |
| // Detectar estrutura de mensagens por heurística | |
| console.log('\n📍 Tentando detectar mensagens por heurística...'); | |
| // Procurar por elementos com "prose" (típico de mensagens) | |
| const proseElements = main.querySelectorAll('[class*="prose"]'); | |
| console.log(`Elementos com prose: ${proseElements.length}`); | |
| if (proseElements.length > 0) { | |
| // Subir hierarquia para encontrar container | |
| const sample = proseElements[0]; | |
| let parent = sample.parentElement; | |
| let levels = 0; | |
| const hierarchy = [sample.tagName + '.' + (sample.className || '').split(' ')[0]]; | |
| while (parent && levels < 10) { | |
| hierarchy.push(parent.tagName + (parent.className ? '.' + parent.className.split(' ')[0] : '')); | |
| parent = parent.parentElement; | |
| levels++; | |
| } | |
| console.log('Hierarquia de prose:', hierarchy.join(' → ')); | |
| report.proseHierarchy = hierarchy; | |
| } | |
| }, | |
| generateRecommendations(report) { | |
| // Container | |
| if (report.workingSelectors.container.length === 0) { | |
| report.recommendations.push('CRÍTICO: Nenhum container encontrado. Verifique se a conversa está aberta.'); | |
| } | |
| // Messages | |
| if (report.workingSelectors.messageGroup.length === 0) { | |
| report.recommendations.push('CRÍTICO: Nenhum grupo de mensagem detectado. Interface pode ter mudado.'); | |
| if (report.foundTestIds && report.foundTestIds.length > 0) { | |
| const turnIds = report.foundTestIds.filter(id => | |
| id.includes('turn') || id.includes('message') || id.includes('conversation') | |
| ); | |
| if (turnIds.length > 0) { | |
| report.recommendations.push(`Sugestão: Tentar seletores com: ${turnIds.join(', ')}`); | |
| } | |
| } | |
| } | |
| // Thinking | |
| if (report.workingSelectors.thinking.length === 0) { | |
| report.recommendations.push('INFO: Nenhum thinking detectado. Pode não existir nesta conversa.'); | |
| } | |
| // Content | |
| if (report.workingSelectors.content.length === 0) { | |
| report.recommendations.push('ALERTA: Seletor de conteúdo não encontrado. Extração pode falhar.'); | |
| } | |
| }, | |
| createCopyButton(report) { | |
| const btn = document.createElement('button'); | |
| btn.textContent = '📋 Copiar Diagnóstico'; | |
| btn.style.cssText = ` | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 99999; | |
| padding: 12px 20px; | |
| background: #7c3aed; | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-family: system-ui; | |
| font-size: 14px; | |
| font-weight: bold; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| `; | |
| btn.onclick = () => { | |
| const text = JSON.stringify(report, null, 2); | |
| navigator.clipboard.writeText(text).then(() => { | |
| btn.textContent = '✅ Copiado!'; | |
| setTimeout(() => { btn.textContent = '📋 Copiar Diagnóstico'; }, 2000); | |
| }); | |
| }; | |
| document.body.appendChild(btn); | |
| } | |
| }; | |
| // ═══════════════════════════════════════════════════════════ | |
| // AUTO-RUN | |
| // ═══════════════════════════════════════════════════════════ | |
| // Esperar página carregar | |
| const waitAndRun = () => { | |
| if (document.querySelector('main')) { | |
| setTimeout(() => { | |
| Diagnostic.run(); | |
| }, 2000); // Esperar 2s para garantir carregamento | |
| } else { | |
| setTimeout(waitAndRun, 1000); | |
| } | |
| }; | |
| // Rodar | |
| if (document.readyState === 'complete') { | |
| waitAndRun(); | |
| } else { | |
| window.addEventListener('load', waitAndRun); | |
| } | |
| // Também expor para execução manual | |
| window.runDiagnostic = () => Diagnostic.run(); | |
| console.log('💡 Claude Extractor Diagnostic carregado. Execute window.runDiagnostic() para rodar manualmente.'); | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Claude Extractor | |
| // @namespace https://github.com/cnmfs/claude-extractor | |
| // @version 2.1.1 | |
| // @description Extrai conversas, projetos, mensagens e artefatos do Claude.ai com hierarquia completa | |
| // @author cnmfs | |
| // @match https://claude.ai/* | |
| // @icon https://claude.ai/favicon.ico | |
| // @grant none | |
| // @run-at document-idle | |
| // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js | |
| // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js | |
| // @license MIT | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| const VERSION = '2.1.1'; | |
| const DEBUG = true; // ATIVADO para diagnóstico | |
| // ═══════════════════════════════════════════════════════════ | |
| // CONFIGURAÇÃO | |
| // ═══════════════════════════════════════════════════════════ | |
| const DEFAULT_CONFIG = { | |
| version: VERSION, | |
| debug: DEBUG, | |
| ui: { | |
| position: { right: 20, bottom: 80 }, | |
| showLabels: true, | |
| collapsed: false, | |
| language: 'pt-BR' | |
| }, | |
| export: { | |
| includeMetadata: true, | |
| includeThinking: true, | |
| includeToolCalls: true, | |
| includeCitations: true, | |
| includeConsole: true, | |
| flatMarkdown: false | |
| } | |
| }; | |
| let CONFIG = JSON.parse(JSON.stringify(DEFAULT_CONFIG)); | |
| // ═══════════════════════════════════════════════════════════ | |
| // TRADUÇÕES | |
| // ═══════════════════════════════════════════════════════════ | |
| const I18N = { | |
| 'pt-BR': { | |
| project: 'Projeto', | |
| conversation: 'Conversa', | |
| message: 'Mensagem', | |
| thinking: 'Thinking', | |
| artifacts: 'Artefatos', | |
| flatMd: 'Flat MD', | |
| settings: 'Config', | |
| diagnose: 'Diagnóstico', | |
| export: 'Exportar', | |
| cancel: 'Cancelar', | |
| close: 'Fechar', | |
| processing: 'Processando...', | |
| extracting: 'Extraindo...', | |
| generating: 'Gerando ZIP...', | |
| noMessages: 'Nenhuma mensagem encontrada', | |
| noThinking: 'Nenhum thinking block', | |
| noArtifacts: 'Nenhum artefato', | |
| noProject: 'Não está em um projeto', | |
| success: 'Sucesso', | |
| error: 'Erro', | |
| previewTitle: 'Preview da Extração', | |
| projectPreview: 'Preview do Projeto', | |
| selectAll: 'Todos', | |
| deselectAll: 'Nenhum', | |
| messages: 'mensagens', | |
| components: 'componentes', | |
| conversations: 'conversas', | |
| files: 'arquivos', | |
| systemPrompt: 'System Prompt', | |
| knowledge: 'Base de Conhecimento', | |
| branches: 'Branches', | |
| selectBranch: 'Selecionar Branch', | |
| mainBranch: 'Principal (M)', | |
| console: 'Console' | |
| }, | |
| 'en': { | |
| project: 'Project', | |
| conversation: 'Conversation', | |
| message: 'Message', | |
| thinking: 'Thinking', | |
| artifacts: 'Artifacts', | |
| flatMd: 'Flat MD', | |
| settings: 'Settings', | |
| diagnose: 'Diagnose', | |
| export: 'Export', | |
| cancel: 'Cancel', | |
| close: 'Close', | |
| processing: 'Processing...', | |
| extracting: 'Extracting...', | |
| generating: 'Generating ZIP...', | |
| noMessages: 'No messages found', | |
| noThinking: 'No thinking blocks', | |
| noArtifacts: 'No artifacts', | |
| noProject: 'Not in a project', | |
| success: 'Success', | |
| error: 'Error', | |
| previewTitle: 'Extraction Preview', | |
| projectPreview: 'Project Preview', | |
| selectAll: 'All', | |
| deselectAll: 'None', | |
| messages: 'messages', | |
| components: 'components', | |
| conversations: 'conversations', | |
| files: 'files', | |
| systemPrompt: 'System Prompt', | |
| knowledge: 'Knowledge Base', | |
| branches: 'Branches', | |
| selectBranch: 'Select Branch', | |
| mainBranch: 'Main (M)', | |
| console: 'Console' | |
| } | |
| }; | |
| const t = (key) => I18N[CONFIG.ui.language]?.[key] || I18N['en'][key] || key; | |
| // ═══════════════════════════════════════════════════════════ | |
| // STORAGE | |
| // ═══════════════════════════════════════════════════════════ | |
| const Storage = { | |
| prefix: 'ce_', | |
| save: (k, v) => { try { localStorage.setItem(Storage.prefix + k, JSON.stringify(v)); } catch(e) {} }, | |
| load: (k, d) => { try { const v = localStorage.getItem(Storage.prefix + k); return v ? JSON.parse(v) : d; } catch(e) { return d; } }, | |
| loadConfig: () => { const s = Storage.load('config'); if (s) CONFIG = {...DEFAULT_CONFIG, ...s, ui: {...DEFAULT_CONFIG.ui, ...(s.ui||{})}, export: {...DEFAULT_CONFIG.export, ...(s.export||{})}}; }, | |
| saveConfig: () => Storage.save('config', { ui: CONFIG.ui, export: CONFIG.export, debug: CONFIG.debug }) | |
| }; | |
| // ═══════════════════════════════════════════════════════════ | |
| // UTILS | |
| // ═══════════════════════════════════════════════════════════ | |
| const Utils = { | |
| log: (...a) => CONFIG.debug && console.log('[CE]', ...a), | |
| warn: (...a) => console.warn('[CE]', ...a), | |
| error: (...a) => console.error('[CE]', ...a), | |
| sanitize: (n) => (n || 'untitled').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[<>:"/\\|?*]/g,'-').replace(/\s+/g,'-').replace(/-+/g,'-').substring(0,60).trim() || 'untitled', | |
| formatDate: () => { | |
| const d = new Date(); | |
| return `${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}-${String(d.getHours()).padStart(2,'0')}${String(d.getMinutes()).padStart(2,'0')}`; | |
| }, | |
| pad: (n, s=3) => String(n).padStart(s, '0'), | |
| sleep: (ms) => new Promise(r => setTimeout(r, ms)), | |
| // MELHORADO: Query com fallbacks mais robustos | |
| query: (sels, p=document) => { | |
| const selArray = Array.isArray(sels) ? sels : [sels]; | |
| for (const s of selArray) { | |
| try { | |
| const e = p.querySelector(s); | |
| if (e) { | |
| Utils.log(`query: "${s}" encontrado`); | |
| return e; | |
| } | |
| } catch(e) { | |
| Utils.log(`query: "${s}" erro: ${e.message}`); | |
| } | |
| } | |
| return null; | |
| }, | |
| // MELHORADO: QueryAll com deduplicação | |
| queryAll: (sels, p=document) => { | |
| const r = new Set(); | |
| const selArray = Array.isArray(sels) ? sels : [sels]; | |
| for (const s of selArray) { | |
| try { | |
| p.querySelectorAll(s).forEach(e => r.add(e)); | |
| } catch(e) { | |
| Utils.log(`queryAll: "${s}" erro: ${e.message}`); | |
| } | |
| } | |
| return Array.from(r); | |
| }, | |
| extractText: (el) => { | |
| if (!el) return ''; | |
| const c = el.cloneNode(true); | |
| c.querySelectorAll('script,style,svg,button,input,form').forEach(e => e.remove()); | |
| return (c.textContent || '').trim(); | |
| }, | |
| // MELHORADO: htmlToMd mais completo | |
| htmlToMd: (el) => { | |
| if (!el) return ''; | |
| const proc = (n) => { | |
| if (n.nodeType === 3) return n.textContent; | |
| if (n.nodeType !== 1) return ''; | |
| const tag = n.tagName.toLowerCase(); | |
| const ch = Array.from(n.childNodes).map(proc).join(''); | |
| switch(tag) { | |
| // Headers | |
| case 'h1': return `\n# ${ch}\n`; | |
| case 'h2': return `\n## ${ch}\n`; | |
| case 'h3': return `\n### ${ch}\n`; | |
| case 'h4': return `\n#### ${ch}\n`; | |
| case 'h5': return `\n##### ${ch}\n`; | |
| case 'h6': return `\n###### ${ch}\n`; | |
| // Emphasis | |
| case 'strong': case 'b': return `**${ch}**`; | |
| case 'em': case 'i': return `*${ch}*`; | |
| case 'del': case 's': return `~~${ch}~~`; | |
| case 'u': return `<u>${ch}</u>`; | |
| case 'mark': return `==${ch}==`; | |
| // Code | |
| case 'code': | |
| const isInPre = n.parentElement?.tagName?.toLowerCase() === 'pre'; | |
| return isInPre ? ch : `\`${ch}\``; | |
| case 'pre': | |
| const code = n.querySelector('code'); | |
| const lang = code?.className?.match(/language-(\w+)/)?.[1] || ''; | |
| const content = code ? code.textContent : n.textContent; | |
| return `\n\`\`\`${lang}\n${content}\n\`\`\`\n`; | |
| // Links | |
| case 'a': | |
| const href = n.getAttribute('href') || ''; | |
| return href ? `[${ch}](${href})` : ch; | |
| // Lists | |
| case 'ul': return '\n' + ch; | |
| case 'ol': return '\n' + ch; | |
| case 'li': | |
| const parent = n.parentElement?.tagName?.toLowerCase(); | |
| const idx = Array.from(n.parentElement?.children || []).indexOf(n); | |
| const prefix = parent === 'ol' ? `${idx + 1}. ` : '- '; | |
| return prefix + ch.trim() + '\n'; | |
| // Blocks | |
| case 'p': return `\n${ch}\n`; | |
| case 'div': return ch.includes('\n') ? ch : `${ch}\n`; | |
| case 'br': return '\n'; | |
| case 'blockquote': return '\n> ' + ch.trim().replace(/\n/g, '\n> ') + '\n'; | |
| case 'hr': return '\n---\n'; | |
| // Tables | |
| case 'table': return '\n' + ch + '\n'; | |
| case 'thead': return ch; | |
| case 'tbody': return ch; | |
| case 'tr': | |
| const cells = Array.from(n.children).map(c => proc(c).trim().replace(/\|/g, '\\|')); | |
| const row = '| ' + cells.join(' | ') + ' |'; | |
| // Adicionar linha separadora após header | |
| if (n.parentElement?.tagName?.toLowerCase() === 'thead') { | |
| const sep = '| ' + cells.map(() => '---').join(' | ') + ' |'; | |
| return row + '\n' + sep + '\n'; | |
| } | |
| return row + '\n'; | |
| case 'th': case 'td': return ch; | |
| // Details/Summary (thinking blocks) | |
| case 'details': | |
| const summary = n.querySelector('summary'); | |
| const summaryText = summary ? Utils.extractText(summary) : 'Details'; | |
| const detailsContent = Array.from(n.childNodes) | |
| .filter(c => c !== summary) | |
| .map(proc).join(''); | |
| return `\n<details>\n<summary>${summaryText}</summary>\n${detailsContent}\n</details>\n`; | |
| case 'summary': return ''; | |
| // Media | |
| case 'img': | |
| const alt = n.getAttribute('alt') || 'image'; | |
| const src = n.getAttribute('src') || ''; | |
| return src ? `` : ''; | |
| // Superscript/Subscript | |
| case 'sup': return `^${ch}^`; | |
| case 'sub': return `~${ch}~`; | |
| // Ignore | |
| case 'script': case 'style': case 'svg': case 'button': | |
| case 'input': case 'form': case 'nav': case 'footer': | |
| return ''; | |
| // Span with potential classes | |
| case 'span': | |
| const cls = n.className || ''; | |
| if (cls.includes('citation')) return `[${ch}]`; | |
| return ch; | |
| // Default: pass through children | |
| default: return ch; | |
| } | |
| }; | |
| return proc(el) | |
| .replace(/\n{3,}/g, '\n\n') | |
| .replace(/^\s+|\s+$/gm, '') | |
| .trim(); | |
| }, | |
| detectLang: (content, lang) => { | |
| const map = { | |
| 'python': 'py', 'py': 'py', 'javascript': 'js', 'js': 'js', 'typescript': 'ts', 'ts': 'ts', | |
| 'jsx': 'jsx', 'tsx': 'tsx', 'html': 'html', 'css': 'css', 'json': 'json', 'yaml': 'yaml', | |
| 'yml': 'yaml', 'sql': 'sql', 'bash': 'sh', 'shell': 'sh', 'sh': 'sh', 'markdown': 'md', 'md': 'md', | |
| 'mermaid': 'mermaid', 'java': 'java', 'c': 'c', 'cpp': 'cpp', 'csharp': 'cs', 'go': 'go', | |
| 'rust': 'rs', 'ruby': 'rb', 'php': 'php', 'swift': 'swift', 'kotlin': 'kt', 'plaintext': 'txt', | |
| 'text': 'txt', 'xml': 'xml', 'toml': 'toml', 'ini': 'ini', 'dockerfile': 'dockerfile', | |
| 'makefile': 'makefile', 'powershell': 'ps1', 'r': 'r', 'scala': 'scala', 'lua': 'lua', | |
| 'perl': 'pl', 'haskell': 'hs', 'elixir': 'ex', 'clojure': 'clj', 'erlang': 'erl' | |
| }; | |
| const l = (lang || '').toLowerCase(); | |
| if (map[l]) return map[l]; | |
| if (content) { | |
| if (content.includes('def ') && content.includes(':')) return 'py'; | |
| if (content.match(/^(function|const|let|var|import|export)/m)) return 'js'; | |
| if (content.match(/<html|<!DOCTYPE/i)) return 'html'; | |
| if (content.startsWith('{') || content.startsWith('[')) { try { JSON.parse(content); return 'json'; } catch(e) {} } | |
| if (content.match(/^(graph|flowchart|sequenceDiagram|classDiagram)/m)) return 'mermaid'; | |
| if (content.match(/^#!/)) return 'sh'; | |
| } | |
| return 'txt'; | |
| } | |
| }; | |
| // ═══════════════════════════════════════════════════════════ | |
| // SELETORES DOM v2.1 - ROBUSTOS | |
| // ═══════════════════════════════════════════════════════════ | |
| const Selectors = { | |
| // Container principal das mensagens | |
| container: [ | |
| '[data-testid="conversation-turn-list"]', | |
| '[data-testid="conversation-messages"]', | |
| // v2.1.1: Novos seletores baseados em diagnóstico | |
| 'div[class*="min-h-"]:has([class*="font-claude"])', | |
| '[class*="conversation-content"]', | |
| '[class*="chat-messages"]', | |
| 'main [class*="overflow-y-auto"]', | |
| 'main [class*="flex-col"]', | |
| '[class*="react-scroll-to-bottom"]', | |
| 'main > div > div', | |
| 'main' | |
| ], | |
| // Grupo de mensagem (cada turno da conversa) | |
| messageGroup: [ | |
| '[data-testid^="conversation-turn"]', | |
| '[data-testid*="turn"]', | |
| '[class*="group/conversation-turn"]', | |
| 'div[class*="group"][class*="relative"]:has([class*="prose"])', | |
| 'div[class*="min-h-"]:has([class*="font-claude"])', | |
| 'div.group:has([class*="prose"])' | |
| ], | |
| // Identificador de mensagem humana | |
| human: [ | |
| '[data-testid="user-message"]', | |
| '[data-testid="human-turn"]', | |
| '[data-testid*="human"]', | |
| '[data-testid*="user"]', | |
| '[class*="human-turn"]', | |
| '[class*="user-message"]' | |
| ], | |
| // Identificador de mensagem do assistente | |
| assistant: [ | |
| '[data-testid="assistant-message"]', | |
| '[data-testid="assistant-turn"]', | |
| '[data-testid*="assistant"]', | |
| '[class*="assistant-turn"]', | |
| '[class*="ai-message"]' | |
| ], | |
| // Conteúdo da mensagem | |
| content: [ | |
| '[class*="prose"]', | |
| '[class*="markdown"]', | |
| '[class*="font-claude-message"]', | |
| '[class*="message-content"]', | |
| '.whitespace-pre-wrap' | |
| ], | |
| // Thinking blocks | |
| thinking: [ | |
| '[data-testid="thinking-block"]', | |
| '[data-testid="thinking-summary"]', | |
| '[data-testid="extended-thinking"]', | |
| '[data-testid*="thinking"]', | |
| 'details[class*="thinking"]', | |
| '[class*="thinking-container"]', | |
| 'details:has(summary)' | |
| ], | |
| // Artefatos/código | |
| artifact: [ | |
| '[data-testid="artifact"]', | |
| '[data-artifact-id]', | |
| '[class*="artifact-container"]', | |
| 'pre:has(code)' | |
| ], | |
| // Chamadas de ferramenta | |
| tools: [ | |
| '[data-testid="tool-use"]', | |
| '[data-testid="tool-call"]', | |
| '[data-testid*="tool"]', | |
| '[class*="tool-use"]', | |
| '[class*="function-call"]' | |
| ], | |
| // Citações | |
| citation: [ | |
| '[data-testid="citation"]', | |
| '[class*="citation"]', | |
| 'a[href^="http"]:not([href*="claude.ai"])' | |
| ], | |
| // Console/Output | |
| console: [ | |
| '[data-testid="code-output"]', | |
| '[data-testid="execution-result"]', | |
| '[class*="code-output"]', | |
| '[class*="execution-result"]', | |
| '[class*="console-output"]', | |
| 'pre[class*="output"]', | |
| '[class*="stdout"]' | |
| ], | |
| // Branches | |
| branch: [ | |
| '[data-testid="branch-selector"]', | |
| '[data-testid="message-versions"]', | |
| '[class*="message-versions"]', | |
| '[class*="branch-indicator"]', | |
| 'button[aria-label*="version"]' | |
| ], | |
| // Projeto | |
| project: { | |
| container: [ | |
| '[data-testid="project-container"]', | |
| '[class*="project-page"]', | |
| 'main[class*="project"]' | |
| ], | |
| title: [ | |
| '[data-testid="project-title"]', | |
| 'h1[class*="project"]', | |
| '[class*="project-header"] h1' | |
| ], | |
| systemPrompt: [ | |
| '[data-testid="project-instructions"]', | |
| '[data-testid="system-prompt"]', | |
| 'textarea[name="instructions"]', | |
| '[class*="system-prompt"]', | |
| '[class*="project-instructions"]' | |
| ], | |
| knowledge: [ | |
| '[data-testid="knowledge-file"]', | |
| '[data-testid="project-file"]', | |
| '[class*="knowledge-item"]', | |
| '[class*="file-item"]' | |
| ], | |
| conversationItem: [ | |
| '[data-testid="conversation-item"]', | |
| '[data-testid="chat-item"]', | |
| 'a[href*="/chat/"]' | |
| ] | |
| } | |
| }; | |
| // ═══════════════════════════════════════════════════════════ | |
| // DETECTOR v2.1 - ROBUSTO | |
| // ═══════════════════════════════════════════════════════════ | |
| const Detector = { | |
| cache: null, | |
| cacheTime: 0, | |
| detect(force = false) { | |
| const now = Date.now(); | |
| if (!force && this.cache && (now - this.cacheTime) < 3000) return this.cache; | |
| Utils.log('=== DETECTANDO ESTRUTURA DOM v2.1.1 ==='); | |
| const result = { | |
| groups: [], | |
| hasThinking: false, | |
| hasArtifacts: false, | |
| hasTools: false, | |
| hasConsole: false, | |
| hasBranches: false, | |
| containerFound: false, | |
| rawGroupCount: 0, | |
| searchRootUsed: 'none', // v2.1.1: Track which root was used | |
| usedBodyFallback: false // v2.1.1: Track if body fallback was needed | |
| }; | |
| // 1. Encontrar container | |
| const container = Utils.query(Selectors.container); | |
| if (!container) { | |
| Utils.warn('Container não encontrado - tentando fallback'); | |
| // Fallback: usar main diretamente | |
| const main = document.querySelector('main'); | |
| if (!main) { | |
| Utils.error('Elemento main não encontrado!'); | |
| return result; | |
| } | |
| Utils.log('Usando main como container fallback'); | |
| } else { | |
| Utils.log('Container encontrado:', container.tagName, container.className?.substring(0, 50)); | |
| result.containerFound = true; | |
| } | |
| let searchRoot = container || document.querySelector('main') || document.body; | |
| result.searchRootUsed = container ? 'container' : (document.querySelector('main') ? 'main' : 'body'); | |
| // 2. Encontrar grupos de mensagens | |
| let groups = Utils.queryAll(Selectors.messageGroup, searchRoot); | |
| Utils.log(`Encontrados ${groups.length} grupos via seletores primários em ${result.searchRootUsed}`); | |
| // PATCH v2.1.1: Se searchRoot restrito não encontrou, buscar no body inteiro | |
| if (groups.length === 0 && searchRoot !== document.body) { | |
| Utils.log('SearchRoot restrito falhou - buscando em document.body...'); | |
| groups = Utils.queryAll(Selectors.messageGroup, document.body); | |
| Utils.log(`Encontrados ${groups.length} grupos em document.body`); | |
| if (groups.length > 0) { | |
| searchRoot = document.body; | |
| result.searchRootUsed = 'body-fallback'; | |
| result.usedBodyFallback = true; | |
| } | |
| } | |
| // Fallback final: buscar divs com conteúdo de mensagem | |
| if (groups.length === 0) { | |
| Utils.log('Aplicando fallback de detecção em document.body...'); | |
| groups = this.fallbackDetection(document.body); | |
| if (groups.length > 0) { | |
| result.searchRootUsed = 'body-fallback-detection'; | |
| result.usedBodyFallback = true; | |
| } | |
| } | |
| result.rawGroupCount = groups.length; | |
| Utils.log(`Total de grupos após fallback: ${groups.length}`); | |
| // 3. Processar cada grupo | |
| groups.forEach((group, idx) => { | |
| const isHuman = this.detectAuthor(group) === 'USER'; | |
| const isAssistant = this.detectAuthor(group) === 'AI'; | |
| // Skip se não identificado e sem conteúdo significativo | |
| if (!isHuman && !isAssistant) { | |
| const hasProseContent = group.querySelector('[class*="prose"]'); | |
| const hasCode = group.querySelector('pre code'); | |
| const textLen = (group.textContent || '').length; | |
| if (!hasProseContent && !hasCode && textLen < 100) { | |
| Utils.log(`Grupo ${idx} ignorado: sem conteúdo significativo`); | |
| return; | |
| } | |
| } | |
| const hasThinking = Utils.query(Selectors.thinking, group) !== null; | |
| const hasArtifacts = group.querySelectorAll('pre code').length > 0 || Utils.query(Selectors.artifact, group) !== null; | |
| const hasTools = Utils.query(Selectors.tools, group) !== null; | |
| const hasConsole = Utils.query(Selectors.console, group) !== null; | |
| const hasBranches = this.detectBranches(group).length > 1; | |
| result.groups.push({ | |
| element: group, | |
| index: idx, | |
| isHuman, | |
| isAssistant: isAssistant || !isHuman, | |
| hasThinking, | |
| hasArtifacts, | |
| hasTools, | |
| hasConsole, | |
| hasBranches, | |
| branches: hasBranches ? this.detectBranches(group) : ['M'] | |
| }); | |
| if (hasThinking) result.hasThinking = true; | |
| if (hasArtifacts) result.hasArtifacts = true; | |
| if (hasTools) result.hasTools = true; | |
| if (hasConsole) result.hasConsole = true; | |
| if (hasBranches) result.hasBranches = true; | |
| }); | |
| this.cache = result; | |
| this.cacheTime = now; | |
| Utils.log(`=== DETECÇÃO COMPLETA: ${result.groups.length} grupos válidos ===`); | |
| return result; | |
| }, | |
| // Fallback de detecção quando seletores primários falham | |
| fallbackDetection(root) { | |
| const groups = []; | |
| // Estratégia 1: Buscar elementos com prose | |
| const proseElements = root.querySelectorAll('[class*="prose"]'); | |
| Utils.log(`Fallback: ${proseElements.length} elementos prose`); | |
| proseElements.forEach(prose => { | |
| // Subir até encontrar o container da mensagem | |
| let parent = prose.parentElement; | |
| let levels = 0; | |
| while (parent && levels < 5) { | |
| const cls = parent.className || ''; | |
| const hasGroup = cls.includes('group') || | |
| cls.includes('relative') || | |
| parent.hasAttribute('data-testid'); | |
| if (hasGroup && !groups.includes(parent)) { | |
| groups.push(parent); | |
| break; | |
| } | |
| parent = parent.parentElement; | |
| levels++; | |
| } | |
| }); | |
| // Estratégia 2: Buscar divs com data-testid relacionados | |
| if (groups.length === 0) { | |
| root.querySelectorAll('[data-testid]').forEach(el => { | |
| const testId = el.getAttribute('data-testid') || ''; | |
| if (testId.includes('turn') || testId.includes('message')) { | |
| if (!groups.includes(el)) groups.push(el); | |
| } | |
| }); | |
| } | |
| // Estratégia 3: Buscar pela estrutura de código | |
| if (groups.length === 0) { | |
| root.querySelectorAll('pre code').forEach(code => { | |
| let parent = code.closest('div[class*="group"]') || | |
| code.closest('div[class*="relative"]'); | |
| if (parent && !groups.includes(parent)) { | |
| groups.push(parent); | |
| } | |
| }); | |
| } | |
| Utils.log(`Fallback retornou ${groups.length} grupos`); | |
| return groups; | |
| }, | |
| // MELHORADO: Detecção robusta de autor | |
| detectAuthor(group) { | |
| // 1. data-testid | |
| const testId = (group.getAttribute('data-testid') || '').toLowerCase(); | |
| if (testId.includes('human') || testId.includes('user')) return 'USER'; | |
| if (testId.includes('assistant') || testId.includes('ai')) return 'AI'; | |
| // 2. Filhos com data-testid | |
| if (group.querySelector('[data-testid*="user"]') || | |
| group.querySelector('[data-testid*="human"]')) return 'USER'; | |
| if (group.querySelector('[data-testid*="assistant"]')) return 'AI'; | |
| // 3. Classes | |
| const cls = (group.className || '').toLowerCase(); | |
| if (cls.includes('human') || cls.includes('user')) return 'USER'; | |
| if (cls.includes('assistant') || cls.includes('ai')) return 'AI'; | |
| // 4. aria-label | |
| const ariaLabel = (group.getAttribute('aria-label') || '').toLowerCase(); | |
| if (ariaLabel.includes('you') || ariaLabel.includes('user')) return 'USER'; | |
| if (ariaLabel.includes('claude') || ariaLabel.includes('assistant')) return 'AI'; | |
| // 5. Heurística: mensagens com thinking/artifacts/code são do AI | |
| if (group.querySelector('details') || | |
| group.querySelector('[class*="thinking"]') || | |
| group.querySelectorAll('pre code').length > 0) { | |
| return 'AI'; | |
| } | |
| // 6. Heurística: texto curto sem formatação provavelmente é humano | |
| const textContent = Utils.extractText(group); | |
| const hasFormatting = group.querySelector('h1,h2,h3,ul,ol,table,blockquote'); | |
| if (textContent.length < 500 && !hasFormatting) { | |
| return 'USER'; | |
| } | |
| return 'UNKNOWN'; | |
| }, | |
| detectBranches(group) { | |
| const branches = ['M']; | |
| const branchElements = Utils.queryAll(Selectors.branch, group); | |
| if (branchElements.length === 0) return branches; | |
| branchElements.forEach((el, idx) => { | |
| const text = el.textContent || el.getAttribute('aria-label') || ''; | |
| const versionMatch = text.match(/(?:vers[ãa]o|version)\s*(\d+)/i); | |
| if (versionMatch) { | |
| branches.push(`T${Utils.pad(parseInt(versionMatch[1]), 2)}`); | |
| } else { | |
| branches.push(`T${Utils.pad(idx + 1, 2)}`); | |
| } | |
| }); | |
| return [...new Set(branches)]; | |
| }, | |
| detectProjectContext() { | |
| const url = window.location.href; | |
| const projectMatch = url.match(/\/project\/([a-f0-9-]+)/i); | |
| if (!projectMatch) { | |
| return { isProject: false, projectId: null, chatId: null }; | |
| } | |
| const chatMatch = url.match(/\/chat\/([a-f0-9-]+)/i); | |
| return { | |
| isProject: true, | |
| projectId: projectMatch[1], | |
| chatId: chatMatch ? chatMatch[1] : null, | |
| isProjectHome: !chatMatch | |
| }; | |
| }, | |
| // DIAGNÓSTICO COMPLETO | |
| diagnose() { | |
| const report = { | |
| url: window.location.href, | |
| timestamp: new Date().toISOString(), | |
| version: VERSION, | |
| projectContext: this.detectProjectContext(), | |
| selectors: {}, | |
| recommendations: [] | |
| }; | |
| console.log('═══════════════════════════════════════════════'); | |
| console.log('🔍 CLAUDE EXTRACTOR v' + VERSION + ' - DIAGNÓSTICO'); | |
| console.log('═══════════════════════════════════════════════'); | |
| console.log('URL:', report.url); | |
| // Testar cada categoria | |
| for (const [name, sels] of Object.entries(Selectors)) { | |
| if (typeof sels === 'object' && !Array.isArray(sels)) { | |
| // Nested (project) | |
| report.selectors[name] = {}; | |
| for (const [subName, subSels] of Object.entries(sels)) { | |
| report.selectors[name][subName] = this.testSelectors(subSels, name + '.' + subName); | |
| } | |
| } else { | |
| report.selectors[name] = this.testSelectors(sels, name); | |
| } | |
| } | |
| // Análise especial | |
| const structure = this.detect(true); | |
| report.detection = { | |
| containerFound: structure.containerFound, | |
| groupsFound: structure.groups.length, | |
| rawGroupCount: structure.rawGroupCount, | |
| searchRootUsed: structure.searchRootUsed, | |
| usedBodyFallback: structure.usedBodyFallback, | |
| hasThinking: structure.hasThinking, | |
| hasArtifacts: structure.hasArtifacts, | |
| hasTools: structure.hasTools, | |
| hasConsole: structure.hasConsole, | |
| hasBranches: structure.hasBranches | |
| }; | |
| console.log('\n📊 RESULTADO DA DETECÇÃO:'); | |
| console.log(' Container:', structure.containerFound ? '✅' : '❌'); | |
| console.log(' SearchRoot:', structure.searchRootUsed); | |
| console.log(' Body Fallback:', structure.usedBodyFallback ? '⚠️ SIM' : 'Não'); | |
| console.log(' Grupos Brutos:', structure.rawGroupCount); | |
| console.log(' Grupos Válidos:', structure.groups.length); | |
| console.log(' Thinking:', structure.hasThinking ? '✅' : '❌'); | |
| console.log(' Artifacts:', structure.hasArtifacts ? '✅' : '❌'); | |
| // Recomendações | |
| if (structure.groups.length === 0) { | |
| report.recommendations.push('CRÍTICO: Nenhum grupo detectado. Verifique se há mensagens na conversa.'); | |
| } | |
| if (!structure.containerFound) { | |
| report.recommendations.push('ALERTA: Container principal não encontrado. Usando fallback.'); | |
| } | |
| console.log('\n💡 RECOMENDAÇÕES:'); | |
| report.recommendations.forEach(r => console.log(' -', r)); | |
| window.CE_DIAGNOSTIC = report; | |
| console.log('\n💾 Report completo em: window.CE_DIAGNOSTIC'); | |
| return report; | |
| }, | |
| testSelectors(sels, name) { | |
| const result = { working: [], failed: [] }; | |
| console.log(`\n📋 ${name}:`); | |
| for (const sel of (Array.isArray(sels) ? sels : [sels])) { | |
| try { | |
| const count = document.querySelectorAll(sel).length; | |
| if (count > 0) { | |
| result.working.push({ selector: sel, count }); | |
| console.log(` ✅ ${sel}: ${count}`); | |
| } else { | |
| result.failed.push(sel); | |
| console.log(` ❌ ${sel}: 0`); | |
| } | |
| } catch(e) { | |
| result.failed.push(sel); | |
| console.log(` ⚠️ ${sel}: ERRO`); | |
| } | |
| } | |
| return result; | |
| } | |
| }; | |
| // ═══════════════════════════════════════════════════════════ | |
| // EXTRACTOR v2.1 | |
| // ═══════════════════════════════════════════════════════════ | |
| const Extractor = { | |
| extractContent(group) { | |
| const contentEl = Utils.query(Selectors.content, group); | |
| if (contentEl) { | |
| const md = Utils.htmlToMd(contentEl); | |
| Utils.log(`extractContent: ${md.length} chars via seletor`); | |
| return md; | |
| } | |
| // Fallback: extrair texto limpo | |
| const text = Utils.extractText(group); | |
| Utils.log(`extractContent: ${text.length} chars via fallback`); | |
| return text.length > 10 ? text : ''; | |
| }, | |
| extractThinking(group) { | |
| const blocks = []; | |
| const containers = Utils.queryAll(Selectors.thinking, group); | |
| Utils.log(`extractThinking: ${containers.length} containers encontrados`); | |
| containers.forEach((c, idx) => { | |
| // Expandir details | |
| if (c.tagName === 'DETAILS' && !c.open) { | |
| c.open = true; | |
| } | |
| let content = ''; | |
| const clone = c.cloneNode(true); | |
| clone.querySelector('summary')?.remove(); | |
| content = Utils.extractText(clone); | |
| if (content && content.length > 20) { | |
| blocks.push({ index: idx + 1, content }); | |
| Utils.log(` Thinking block ${idx+1}: ${content.length} chars`); | |
| } | |
| }); | |
| return blocks; | |
| }, | |
| extractArtifacts(group) { | |
| const artifacts = []; | |
| const seen = new Set(); | |
| // Containers de artefato | |
| Utils.queryAll(Selectors.artifact, group).forEach(c => { | |
| c.querySelectorAll('pre code, code').forEach(code => { | |
| const content = code.textContent || ''; | |
| if (content.length < 10 || seen.has(content)) return; | |
| seen.add(content); | |
| const langMatch = code.className.match(/language-(\w+)/); | |
| const lang = langMatch ? langMatch[1] : ''; | |
| const ext = Utils.detectLang(content, lang); | |
| artifacts.push({ index: artifacts.length + 1, content, language: lang, ext }); | |
| }); | |
| }); | |
| // Pre code diretos | |
| group.querySelectorAll('pre').forEach(pre => { | |
| const code = pre.querySelector('code') || pre; | |
| const content = code.textContent || ''; | |
| if (content.length < 10 || seen.has(content)) return; | |
| seen.add(content); | |
| const langMatch = (code.className + ' ' + pre.className).match(/language-(\w+)/); | |
| const lang = langMatch ? langMatch[1] : ''; | |
| const ext = Utils.detectLang(content, lang); | |
| artifacts.push({ index: artifacts.length + 1, content, language: lang, ext }); | |
| }); | |
| Utils.log(`extractArtifacts: ${artifacts.length} artefatos`); | |
| return artifacts; | |
| }, | |
| extractTools(group) { | |
| const tools = []; | |
| Utils.queryAll(Selectors.tools, group).forEach((el, idx) => { | |
| const content = Utils.extractText(el); | |
| if (content) tools.push({ type: 'tool', index: idx + 1, content }); | |
| }); | |
| Utils.log(`extractTools: ${tools.length} tools`); | |
| return tools; | |
| }, | |
| extractCitations(group) { | |
| const citations = []; | |
| const seen = new Set(); | |
| Utils.queryAll(Selectors.citation, group).forEach(el => { | |
| const href = el.getAttribute('href'); | |
| if (!href || !href.startsWith('http') || seen.has(href)) return; | |
| seen.add(href); | |
| citations.push({ text: el.textContent?.trim() || href, url: href }); | |
| }); | |
| return citations; | |
| }, | |
| extractConsole(group) { | |
| const outputs = []; | |
| Utils.queryAll(Selectors.console, group).forEach((el, idx) => { | |
| const content = el.textContent?.trim() || ''; | |
| if (content.length < 5) return; | |
| let outputType = 'stdout'; | |
| const cls = (el.className || '').toLowerCase(); | |
| if (cls.includes('stderr') || cls.includes('error')) { | |
| outputType = 'stderr'; | |
| } | |
| outputs.push({ | |
| index: idx + 1, | |
| type: outputType, | |
| content | |
| }); | |
| }); | |
| Utils.log(`extractConsole: ${outputs.length} outputs`); | |
| return outputs; | |
| }, | |
| extractAll(selectedBranch = 'M') { | |
| Utils.log('=== EXTRAINDO MENSAGENS ==='); | |
| const messages = []; | |
| const structure = Detector.detect(true); | |
| if (structure.groups.length === 0) { | |
| Utils.warn('Nenhum grupo detectado'); | |
| return messages; | |
| } | |
| structure.groups.forEach((g, idx) => { | |
| const author = g.isHuman ? 'USER' : 'AI'; | |
| let branch = selectedBranch; | |
| if (g.hasBranches && g.branches.includes(selectedBranch)) { | |
| branch = selectedBranch; | |
| } else { | |
| branch = 'M'; | |
| } | |
| const msg = { | |
| index: idx + 1, | |
| author, | |
| branch, | |
| availableBranches: g.branches || ['M'], | |
| components: [], | |
| stats: { thinking: 0, artifacts: 0, tools: 0, citations: 0, console: 0 } | |
| }; | |
| let seq = 0; | |
| // Thinking | |
| if (author === 'AI' && CONFIG.export.includeThinking) { | |
| const thinking = this.extractThinking(g.element); | |
| thinking.forEach(tb => { | |
| seq++; | |
| msg.components.push({ seq, type: 'thinking', content: tb.content, ext: 'md' }); | |
| msg.stats.thinking++; | |
| }); | |
| } | |
| // Conteúdo principal | |
| const content = this.extractContent(g.element); | |
| if (content && content.length > 0) { | |
| seq++; | |
| msg.components.push({ seq, type: 'message', content, ext: 'md' }); | |
| } | |
| // Artefatos | |
| const artifacts = this.extractArtifacts(g.element); | |
| artifacts.forEach(a => { | |
| seq++; | |
| msg.components.push({ seq, type: 'artifact', content: a.content, language: a.language, ext: a.ext }); | |
| msg.stats.artifacts++; | |
| }); | |
| // Tools | |
| if (author === 'AI' && CONFIG.export.includeToolCalls) { | |
| const tools = this.extractTools(g.element); | |
| tools.forEach(t => { | |
| seq++; | |
| msg.components.push({ seq, type: 'tool', content: t.content, ext: 'json' }); | |
| msg.stats.tools++; | |
| }); | |
| } | |
| // Citations | |
| if (CONFIG.export.includeCitations) { | |
| const citations = this.extractCitations(g.element); | |
| if (citations.length > 0) { | |
| seq++; | |
| const citContent = citations.map(c => `- [${c.text}](${c.url})`).join('\n'); | |
| msg.components.push({ seq, type: 'citations', content: citContent, ext: 'md' }); | |
| msg.stats.citations = citations.length; | |
| } | |
| } | |
| // Console | |
| if (author === 'AI' && CONFIG.export.includeConsole) { | |
| const consoleOutputs = this.extractConsole(g.element); | |
| consoleOutputs.forEach(co => { | |
| seq++; | |
| msg.components.push({ seq, type: 'console', content: co.content, outputType: co.type, ext: 'txt' }); | |
| msg.stats.console++; | |
| }); | |
| } | |
| if (msg.components.length > 0) { | |
| messages.push(msg); | |
| } | |
| }); | |
| Utils.log(`=== EXTRAÇÃO COMPLETA: ${messages.length} mensagens ===`); | |
| return messages; | |
| }, | |
| extractProjectInfo() { | |
| Utils.log('Extraindo info do projeto...'); | |
| const info = { | |
| title: '', | |
| systemPrompt: '', | |
| knowledge: [], | |
| conversations: [] | |
| }; | |
| // Título | |
| const titleEl = Utils.query(Selectors.project.title); | |
| if (titleEl) { | |
| info.title = Utils.extractText(titleEl); | |
| Utils.log('Título:', info.title); | |
| } | |
| // System Prompt | |
| const promptEl = Utils.query(Selectors.project.systemPrompt); | |
| if (promptEl) { | |
| info.systemPrompt = promptEl.value || Utils.extractText(promptEl); | |
| Utils.log('System prompt:', info.systemPrompt.length, 'chars'); | |
| } | |
| // Knowledge files | |
| const fileElements = Utils.queryAll(Selectors.project.knowledge); | |
| Utils.log('Knowledge elements:', fileElements.length); | |
| fileElements.forEach((el, idx) => { | |
| const name = Utils.extractText(el) || `file_${idx+1}`; | |
| info.knowledge.push({ index: idx + 1, name, element: el }); | |
| }); | |
| // Conversations | |
| const convElements = Utils.queryAll(Selectors.project.conversationItem); | |
| Utils.log('Conversation elements:', convElements.length); | |
| convElements.forEach((el, idx) => { | |
| const title = Utils.extractText(el) || `chat_${idx+1}`; | |
| const href = el.getAttribute('href') || ''; | |
| const chatIdMatch = href.match(/\/chat\/([a-f0-9-]+)/i); | |
| info.conversations.push({ | |
| index: idx + 1, | |
| title, | |
| chatId: chatIdMatch ? chatIdMatch[1] : null, | |
| href | |
| }); | |
| }); | |
| return info; | |
| }, | |
| summary(msgs) { | |
| return { | |
| total: msgs.length, | |
| user: msgs.filter(m => m.author === 'USER').length, | |
| ai: msgs.filter(m => m.author === 'AI').length, | |
| thinking: msgs.reduce((s, m) => s + m.stats.thinking, 0), | |
| artifacts: msgs.reduce((s, m) => s + m.stats.artifacts, 0), | |
| tools: msgs.reduce((s, m) => s + m.stats.tools, 0), | |
| console: msgs.reduce((s, m) => s + (m.stats.console || 0), 0) | |
| }; | |
| } | |
| }; | |
| // ═══════════════════════════════════════════════════════════ | |
| // FORMATTER | |
| // ═══════════════════════════════════════════════════════════ | |
| const Formatter = { | |
| message(msg, opts = {}) { | |
| const { includeHeader = true, flat = false } = opts; | |
| let content = ''; | |
| if (includeHeader) { | |
| const icon = msg.author === 'USER' ? '👤' : '🤖'; | |
| content += `## ${icon} ${msg.author} [${Utils.pad(msg.index)}]\n\n`; | |
| } | |
| msg.components.forEach(comp => { | |
| switch(comp.type) { | |
| case 'thinking': | |
| content += `<details>\n<summary>💭 Thinking</summary>\n\n${comp.content}\n\n</details>\n\n`; | |
| break; | |
| case 'message': | |
| content += comp.content + '\n\n'; | |
| break; | |
| case 'artifact': | |
| const lang = comp.language || comp.ext || ''; | |
| content += `\`\`\`${lang}\n${comp.content}\n\`\`\`\n\n`; | |
| break; | |
| case 'tool': | |
| content += `<details>\n<summary>🔧 Tool Call</summary>\n\n\`\`\`json\n${comp.content}\n\`\`\`\n\n</details>\n\n`; | |
| break; | |
| case 'citations': | |
| content += `**📚 Citations:**\n${comp.content}\n\n`; | |
| break; | |
| case 'console': | |
| const consoleType = comp.outputType === 'stderr' ? '❌ stderr' : '📤 stdout'; | |
| content += `**${consoleType}:**\n\`\`\`\n${comp.content}\n\`\`\`\n\n`; | |
| break; | |
| } | |
| }); | |
| return content; | |
| }, | |
| conversation(msgs, title = 'Conversation') { | |
| let content = `# ${title}\n\n`; | |
| content += `> Extracted: ${new Date().toISOString()}\n`; | |
| content += `> Messages: ${msgs.length}\n\n`; | |
| content += '---\n\n'; | |
| msgs.forEach(msg => { | |
| content += this.message(msg); | |
| content += '---\n\n'; | |
| }); | |
| return content; | |
| }, | |
| flat(msgs, title = 'Conversation') { | |
| return this.conversation(msgs, title); | |
| } | |
| }; | |
| // ═══════════════════════════════════════════════════════════ | |
| // EXPORTER | |
| // ═══════════════════════════════════════════════════════════ | |
| const Exporter = { | |
| async toZip(msgs, options = {}) { | |
| const { name = 'claude-export', includeFlat = true } = options; | |
| if (typeof JSZip === 'undefined') { | |
| throw new Error('JSZip não carregado. Recarregue a página.'); | |
| } | |
| const zip = new JSZip(); | |
| const baseName = Utils.sanitize(name); | |
| const folder = zip.folder(baseName); | |
| // Metadata | |
| const meta = { | |
| version: VERSION, | |
| exported: new Date().toISOString(), | |
| url: window.location.href, | |
| summary: Extractor.summary(msgs) | |
| }; | |
| folder.file('_metadata.json', JSON.stringify(meta, null, 2)); | |
| // Mensagens individuais | |
| const messagesFolder = folder.folder('messages'); | |
| msgs.forEach(msg => { | |
| const msgFolder = messagesFolder.folder(`${Utils.pad(msg.index)}_${msg.author}`); | |
| msg.components.forEach(comp => { | |
| const filename = `${Utils.pad(comp.seq)}_${comp.type}.${comp.ext}`; | |
| msgFolder.file(filename, comp.content); | |
| }); | |
| }); | |
| // Flat markdown | |
| if (includeFlat) { | |
| const flatContent = Formatter.flat(msgs, baseName); | |
| folder.file('_full_conversation.md', flatContent); | |
| } | |
| // Gerar ZIP | |
| const blob = await zip.generateAsync({ type: 'blob' }); | |
| return { blob, filename: `${baseName}-${Utils.formatDate()}.zip` }; | |
| }, | |
| async download(msgs, options = {}) { | |
| try { | |
| const { blob, filename } = await this.toZip(msgs, options); | |
| if (typeof saveAs === 'function') { | |
| saveAs(blob, filename); | |
| } else { | |
| // Fallback | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| return { success: true, filename }; | |
| } catch (e) { | |
| Utils.error('Erro no download:', e); | |
| return { success: false, error: e.message }; | |
| } | |
| }, | |
| downloadFlat(msgs, options = {}) { | |
| const { name = 'claude-conversation' } = options; | |
| const content = Formatter.flat(msgs, name); | |
| const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }); | |
| const filename = `${Utils.sanitize(name)}-${Utils.formatDate()}.md`; | |
| if (typeof saveAs === 'function') { | |
| saveAs(blob, filename); | |
| } else { | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| return { success: true, filename }; | |
| } | |
| }; | |
| // ═══════════════════════════════════════════════════════════ | |
| // UI | |
| // ═══════════════════════════════════════════════════════════ | |
| const UI = { | |
| container: null, | |
| modal: null, | |
| init() { | |
| if (this.container) return; | |
| this.injectStyles(); | |
| this.createContainer(); | |
| this.createButtons(); | |
| Utils.log('UI inicializada'); | |
| }, | |
| injectStyles() { | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| .ce-container { | |
| position: fixed; | |
| z-index: 99999; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| font-family: system-ui, -apple-system, sans-serif; | |
| } | |
| .ce-btn { | |
| padding: 10px 16px; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 13px; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| transition: all 0.2s; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.15); | |
| } | |
| .ce-btn:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| } | |
| .ce-btn-primary { | |
| background: linear-gradient(135deg, #7c3aed, #5b21b6); | |
| color: white; | |
| } | |
| .ce-btn-secondary { | |
| background: white; | |
| color: #374151; | |
| border: 1px solid #e5e7eb; | |
| } | |
| .ce-btn-success { | |
| background: linear-gradient(135deg, #10b981, #059669); | |
| color: white; | |
| } | |
| .ce-btn-warning { | |
| background: linear-gradient(135deg, #f59e0b, #d97706); | |
| color: white; | |
| } | |
| .ce-modal { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0,0,0,0.5); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 100000; | |
| } | |
| .ce-modal-content { | |
| background: white; | |
| border-radius: 16px; | |
| max-width: 600px; | |
| width: 90%; | |
| max-height: 80vh; | |
| overflow: auto; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.3); | |
| } | |
| .ce-modal-header { | |
| padding: 20px; | |
| border-bottom: 1px solid #e5e7eb; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .ce-modal-header h2 { | |
| margin: 0; | |
| font-size: 18px; | |
| color: #111827; | |
| } | |
| .ce-modal-body { | |
| padding: 20px; | |
| } | |
| .ce-modal-footer { | |
| padding: 16px 20px; | |
| border-top: 1px solid #e5e7eb; | |
| display: flex; | |
| gap: 12px; | |
| justify-content: flex-end; | |
| } | |
| .ce-stat { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 8px 0; | |
| border-bottom: 1px solid #f3f4f6; | |
| } | |
| .ce-stat:last-child { | |
| border-bottom: none; | |
| } | |
| .ce-close { | |
| background: none; | |
| border: none; | |
| font-size: 24px; | |
| cursor: pointer; | |
| color: #6b7280; | |
| padding: 0; | |
| line-height: 1; | |
| } | |
| .ce-close:hover { | |
| color: #111827; | |
| } | |
| .ce-code { | |
| background: #f3f4f6; | |
| padding: 12px; | |
| border-radius: 8px; | |
| font-family: monospace; | |
| font-size: 12px; | |
| overflow: auto; | |
| max-height: 300px; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| }, | |
| createContainer() { | |
| this.container = document.createElement('div'); | |
| this.container.className = 'ce-container'; | |
| this.container.style.right = CONFIG.ui.position.right + 'px'; | |
| this.container.style.bottom = CONFIG.ui.position.bottom + 'px'; | |
| document.body.appendChild(this.container); | |
| }, | |
| createButtons() { | |
| const buttons = [ | |
| { icon: '📥', label: t('export'), action: () => this.showExportPreview(), class: 'ce-btn-primary' }, | |
| { icon: '📄', label: t('flatMd'), action: () => this.exportFlat(), class: 'ce-btn-secondary' }, | |
| { icon: '💭', label: t('thinking'), action: () => this.showThinkingPreview(), class: 'ce-btn-secondary' }, | |
| { icon: '🔍', label: t('diagnose'), action: () => this.showDiagnose(), class: 'ce-btn-warning' } | |
| ]; | |
| buttons.forEach(btn => { | |
| const el = document.createElement('button'); | |
| el.className = `ce-btn ${btn.class}`; | |
| el.innerHTML = `${btn.icon}${CONFIG.ui.showLabels ? ' ' + btn.label : ''}`; | |
| el.onclick = btn.action; | |
| this.container.appendChild(el); | |
| }); | |
| }, | |
| showModal(title, content, actions = []) { | |
| this.closeModal(); | |
| this.modal = document.createElement('div'); | |
| this.modal.className = 'ce-modal'; | |
| this.modal.innerHTML = ` | |
| <div class="ce-modal-content"> | |
| <div class="ce-modal-header"> | |
| <h2>${title}</h2> | |
| <button class="ce-close">×</button> | |
| </div> | |
| <div class="ce-modal-body">${content}</div> | |
| <div class="ce-modal-footer"></div> | |
| </div> | |
| `; | |
| this.modal.querySelector('.ce-close').onclick = () => this.closeModal(); | |
| this.modal.onclick = (e) => { if (e.target === this.modal) this.closeModal(); }; | |
| const footer = this.modal.querySelector('.ce-modal-footer'); | |
| actions.forEach(act => { | |
| const btn = document.createElement('button'); | |
| btn.className = `ce-btn ${act.class || 'ce-btn-secondary'}`; | |
| btn.textContent = act.label; | |
| btn.onclick = act.action; | |
| footer.appendChild(btn); | |
| }); | |
| document.body.appendChild(this.modal); | |
| }, | |
| closeModal() { | |
| if (this.modal) { | |
| this.modal.remove(); | |
| this.modal = null; | |
| } | |
| }, | |
| showExportPreview() { | |
| const msgs = Extractor.extractAll(); | |
| const summary = Extractor.summary(msgs); | |
| if (msgs.length === 0) { | |
| this.showModal(t('error'), `<p>❌ ${t('noMessages')}</p>`, [ | |
| { label: t('close'), action: () => this.closeModal() } | |
| ]); | |
| return; | |
| } | |
| const content = ` | |
| <div class="ce-stat"><span>📝 ${t('messages')}</span><strong>${summary.total}</strong></div> | |
| <div class="ce-stat"><span>👤 User</span><strong>${summary.user}</strong></div> | |
| <div class="ce-stat"><span>🤖 AI</span><strong>${summary.ai}</strong></div> | |
| <div class="ce-stat"><span>💭 Thinking</span><strong>${summary.thinking}</strong></div> | |
| <div class="ce-stat"><span>📦 Artifacts</span><strong>${summary.artifacts}</strong></div> | |
| <div class="ce-stat"><span>🔧 Tools</span><strong>${summary.tools}</strong></div> | |
| <div class="ce-stat"><span>📤 Console</span><strong>${summary.console}</strong></div> | |
| `; | |
| this.showModal(t('previewTitle'), content, [ | |
| { label: t('cancel'), action: () => this.closeModal() }, | |
| { label: `📥 ${t('export')} ZIP`, class: 'ce-btn-success', action: async () => { | |
| const result = await Exporter.download(msgs, { name: this.getConversationTitle() }); | |
| if (result.success) { | |
| this.closeModal(); | |
| this.toast(`✅ ${result.filename}`); | |
| } else { | |
| this.toast(`❌ ${result.error}`, 'error'); | |
| } | |
| }} | |
| ]); | |
| }, | |
| showThinkingPreview() { | |
| const msgs = Extractor.extractAll(); | |
| const thinkingBlocks = msgs.flatMap(m => | |
| m.components.filter(c => c.type === 'thinking') | |
| ); | |
| if (thinkingBlocks.length === 0) { | |
| this.showModal(t('thinking'), `<p>❌ ${t('noThinking')}</p>`, [ | |
| { label: t('close'), action: () => this.closeModal() } | |
| ]); | |
| return; | |
| } | |
| const content = thinkingBlocks.map((tb, idx) => | |
| `<details style="margin-bottom:12px"> | |
| <summary style="cursor:pointer;font-weight:500">💭 Block ${idx+1} (${tb.content.length} chars)</summary> | |
| <div class="ce-code" style="margin-top:8px">${tb.content.substring(0, 500)}${tb.content.length > 500 ? '...' : ''}</div> | |
| </details>` | |
| ).join(''); | |
| this.showModal(`💭 ${t('thinking')} (${thinkingBlocks.length})`, content, [ | |
| { label: t('close'), action: () => this.closeModal() } | |
| ]); | |
| }, | |
| showDiagnose() { | |
| const report = Detector.diagnose(); | |
| let content = `<p><strong>URL:</strong> ${report.url}</p>`; | |
| content += `<p><strong>Version:</strong> ${report.version}</p>`; | |
| content += `<h3 style="margin-top:16px">📊 Detecção</h3>`; | |
| content += `<div class="ce-stat"><span>Container</span><strong>${report.detection.containerFound ? '✅' : '❌'}</strong></div>`; | |
| content += `<div class="ce-stat"><span>Grupos encontrados</span><strong>${report.detection.groupsFound}</strong></div>`; | |
| content += `<div class="ce-stat"><span>Thinking</span><strong>${report.detection.hasThinking ? '✅' : '❌'}</strong></div>`; | |
| content += `<div class="ce-stat"><span>Artifacts</span><strong>${report.detection.hasArtifacts ? '✅' : '❌'}</strong></div>`; | |
| if (report.recommendations.length > 0) { | |
| content += `<h3 style="margin-top:16px">💡 Recomendações</h3>`; | |
| content += `<ul>${report.recommendations.map(r => `<li>${r}</li>`).join('')}</ul>`; | |
| } | |
| content += `<h3 style="margin-top:16px">📋 Report Completo</h3>`; | |
| content += `<p>Abra o console (F12) ou copie: <code>window.CE_DIAGNOSTIC</code></p>`; | |
| this.showModal('🔍 ' + t('diagnose'), content, [ | |
| { label: '📋 Copiar JSON', action: () => { | |
| navigator.clipboard.writeText(JSON.stringify(report, null, 2)); | |
| this.toast('✅ Copiado!'); | |
| }}, | |
| { label: t('close'), action: () => this.closeModal() } | |
| ]); | |
| }, | |
| exportFlat() { | |
| const msgs = Extractor.extractAll(); | |
| if (msgs.length === 0) { | |
| this.toast(`❌ ${t('noMessages')}`, 'error'); | |
| return; | |
| } | |
| const result = Exporter.downloadFlat(msgs, { name: this.getConversationTitle() }); | |
| if (result.success) { | |
| this.toast(`✅ ${result.filename}`); | |
| } | |
| }, | |
| getConversationTitle() { | |
| // Tentar obter título da página | |
| const h1 = document.querySelector('h1'); | |
| if (h1) return Utils.extractText(h1); | |
| // Fallback para título da página | |
| const title = document.title.replace(/Claude/gi, '').trim(); | |
| if (title) return title; | |
| return 'claude-conversation'; | |
| }, | |
| toast(message, type = 'success') { | |
| const toast = document.createElement('div'); | |
| toast.style.cssText = ` | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| padding: 12px 24px; | |
| background: ${type === 'error' ? '#ef4444' : '#10b981'}; | |
| color: white; | |
| border-radius: 8px; | |
| font-family: system-ui; | |
| font-size: 14px; | |
| z-index: 100001; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| `; | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| setTimeout(() => toast.remove(), 3000); | |
| } | |
| }; | |
| // ═══════════════════════════════════════════════════════════ | |
| // INIT | |
| // ═══════════════════════════════════════════════════════════ | |
| function init() { | |
| Storage.loadConfig(); | |
| // Aguardar página carregar | |
| const waitForPage = () => { | |
| if (document.querySelector('main')) { | |
| Utils.log('=== CLAUDE EXTRACTOR v' + VERSION + ' INICIADO ==='); | |
| Utils.log('URL:', location.href); | |
| // Inicializar UI após pequeno delay | |
| setTimeout(() => { | |
| UI.init(); | |
| // Rodar diagnóstico inicial | |
| const structure = Detector.detect(true); | |
| Utils.log('Diagnóstico inicial:', { | |
| container: structure.containerFound, | |
| groups: structure.groups.length, | |
| thinking: structure.hasThinking, | |
| artifacts: structure.hasArtifacts | |
| }); | |
| }, 1000); | |
| } else { | |
| setTimeout(waitForPage, 500); | |
| } | |
| }; | |
| if (document.readyState === 'complete') { | |
| waitForPage(); | |
| } else { | |
| window.addEventListener('load', waitForPage); | |
| } | |
| } | |
| // Expor para debug | |
| window.ClaudeExtractor = { | |
| VERSION, | |
| Detector, | |
| Extractor, | |
| Formatter, | |
| Exporter, | |
| UI, | |
| Utils, | |
| diagnose: () => Detector.diagnose(), | |
| extract: () => Extractor.extractAll() | |
| }; | |
| init(); | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Z.ai Mobile Collector Pro | |
| // @namespace ZaiIntelligence | |
| // @version 6.0.0 | |
| // @description Coletor de dados otimizado para chat.z.ai (Mobile). Captura diálogos e execuções de forma resiliente. | |
| // @author Z.ai Engineering | |
| // @match *://chat.z.ai/* | |
| // @icon https://chat.z.ai/favicon.ico | |
| // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js | |
| // @grant GM_addStyle | |
| // @grant GM_setClipboard | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| const STATE = { | |
| data: [], | |
| isExpanded: false, | |
| lastCaptureCount: 0 | |
| }; | |
| // ========================================== | |
| // Módulo 1: UI Mobile-First (Resiliente) | |
| // ========================================== | |
| function injectMobileUI() { | |
| const container = document.createElement('div'); | |
| container.id = 'zai-mobile-wrapper'; | |
| container.innerHTML = ` | |
| <div id="zai-minimized">Z</div> | |
| <div id="zai-expanded" style="display:none;"> | |
| <div class="zai-header"> | |
| <span>Z.ai Collector (Mobile)</span> | |
| <button id="zai-close-btn">_</button> | |
| </div> | |
| <div class="zai-content"> | |
| <div id="zai-count">Itens: 0</div> | |
| <div id="zai-status">Monitorando chat.z.ai...</div> | |
| <div class="zai-actions"> | |
| <button id="zai-copy">Copiar JSON</button> | |
| <button id="zai-save" class="primary">Salvar .JSON</button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(container); | |
| GM_addStyle(` | |
| #zai-mobile-wrapper { position: fixed; top: 10px; right: 10px; z-index: 99999; font-family: sans-serif; } | |
| #zai-minimized { width: 44px; height: 44px; background: #00ff88; color: #000; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; box-shadow: 0 4px 12px rgba(0,0,0,0.3); cursor: pointer; border: 2px solid #000; } | |
| #zai-expanded { width: 280px; background: #1a1a1a; border: 1px solid #333; border-radius: 8px; color: #fff; overflow: hidden; box-shadow: 0 8px 24px rgba(0,0,0,0.5); } | |
| .zai-header { padding: 10px; background: #000; display: flex; justify-content: space-between; align-items: center; font-size: 12px; border-bottom: 1px solid #333; } | |
| #zai-close-btn { background: none; border: none; color: #fff; font-size: 20px; cursor: pointer; } | |
| .zai-content { padding: 15px; } | |
| #zai-count { font-size: 18px; font-weight: bold; color: #00ff88; margin-bottom: 5px; } | |
| #zai-status { font-size: 10px; color: #aaa; margin-bottom: 15px; } | |
| .zai-actions { display: flex; gap: 8px; } | |
| .zai-actions button { flex: 1; padding: 10px 5px; font-size: 11px; border-radius: 4px; border: 1px solid #444; background: #222; color: #fff; } | |
| .zai-actions button.primary { background: #00ff88; color: #000; font-weight: bold; border: none; } | |
| `); | |
| // Eventos de Toque (Mobile) | |
| document.getElementById('zai-minimized').onclick = toggleUI; | |
| document.getElementById('zai-close-btn').onclick = toggleUI; | |
| document.getElementById('zai-copy').onclick = copyData; | |
| document.getElementById('zai-save').onclick = saveData; | |
| } | |
| function toggleUI() { | |
| STATE.isExpanded = !STATE.isExpanded; | |
| document.getElementById('zai-minimized').style.display = STATE.isExpanded ? 'none' : 'flex'; | |
| document.getElementById('zai-expanded').style.display = STATE.isExpanded ? 'block' : 'none'; | |
| } | |
| // ========================================== | |
| // Módulo 2: Motor de Captura Heurística | |
| // ========================================== | |
| // Esta função não depende de classes fixas, ela busca por estruturas de mensagens | |
| function smartCapture() { | |
| // Busca por elementos que costumam conter mensagens em chats de IA | |
| // chat.z.ai geralmente usa tags de markdown ou divs com pre-wrap | |
| const messageElements = document.querySelectorAll('div[class*="message"], div[class*="content"], .markdown-body'); | |
| let sessionData = []; | |
| messageElements.forEach((el) => { | |
| const text = el.innerText.trim(); | |
| if (text.length < 2) return; // Ignora ruído | |
| // Evita duplicatas na mesma sessão de scan | |
| if (!sessionData.includes(text)) { | |
| sessionData.push(text); | |
| } | |
| }); | |
| // Se detectou mudança no volume de dados, atualiza o estado | |
| if (sessionData.length > STATE.lastCaptureCount) { | |
| STATE.data = sessionData.map((t, i) => ({ | |
| id: i, | |
| timestamp: new Date().toISOString(), | |
| content: t, | |
| platform: "chat.z.ai-mobile" | |
| })); | |
| STATE.lastCaptureCount = sessionData.length; | |
| updateUI(); | |
| } | |
| } | |
| function updateUI() { | |
| const el = document.getElementById('zai-count'); | |
| if (el) el.innerText = `Itens: ${STATE.data.length}`; | |
| } | |
| function copyData() { | |
| GM_setClipboard(JSON.stringify(STATE.data, null, 2)); | |
| alert("Dados copiados!"); | |
| } | |
| function saveData() { | |
| const blob = new Blob([JSON.stringify(STATE.data, null, 2)], {type: "application/json"}); | |
| saveAs(blob, `Zai_Mobile_Chat_${Date.now()}.json`); | |
| } | |
| // ========================================== | |
| // Módulo 3: Validação e Ciclo de Vida | |
| // ========================================== | |
| function init() { | |
| console.log("[Z.ai] Inicializando Coletor Mobile..."); | |
| injectMobileUI(); | |
| // Observer de alta sensibilidade para Mobile | |
| const observer = new MutationObserver(() => { | |
| // Debounce simples para não sobrecarregar o processador do celular | |
| clearTimeout(window.zaiTimeout); | |
| window.zaiTimeout = setTimeout(smartCapture, 1000); | |
| }); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true, | |
| characterData: true | |
| }); | |
| } | |
| // Garante execução no chat.z.ai | |
| if (location.hostname.includes('z.ai')) { | |
| init(); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment