// Gerenciador de Bookmarklets para Hugging Face
// Substitui os bookmarklets problemáticos por uma solução mais robusta
class BookmarkletManager {
constructor() {
this.isInjected = false;
this.currentPanel = null;
this.aiType = null;
}
// Detectar qual página do Hugging Face estamos
detectAIType() {
const url = window.location.href;
if (url.includes('Qwen') || url.includes('qwen')) {
return 'Qwen';
} else if (url.includes('zai-org') || url.includes('zai')) {
return 'Zai';
}
return 'HF';
}
// Injetar o painel principal
injectMainPanel() {
if (this.isInjected) return;
this.isInjected = true;
this.aiType = this.detectAIType();
// Remover painel existente
const existing = document.getElementById('hf-bookmarklet-panel');
if (existing) existing.remove();
// Criar painel flutuante
this.currentPanel = document.createElement('div');
this.currentPanel.id = 'hf-bookmarklet-panel';
this.applyPanelStyles();
this.currentPanel.innerHTML = this.getPanelHTML();
document.body.appendChild(this.currentPanel);
this.setupEventListeners();
this.checkForIncomingData();
// Auto-detectar e colar dados recebidos
setTimeout(() => this.autoPasteFromClipboard(), 1000);
}
applyPanelStyles() {
const style = document.createElement('style');
style.textContent = `
#hf-bookmarklet-panel {
position: fixed !important;
top: 20px !important;
right: 20px !important;
width: 350px !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border-radius: 16px !important;
padding: 0 !important;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04) !important;
z-index: 2147483647 !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
font-size: 14px !important;
color: white !important;
backdrop-filter: blur(10px) !important;
animation: slideInRight 0.3s ease-out !important;
}
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
#hf-bookmarklet-panel .panel-header {
background: rgba(255, 255, 255, 0.1) !important;
padding: 20px !important;
border-radius: 16px 16px 0 0 !important;
backdrop-filter: blur(10px) !important;
}
#hf-bookmarklet-panel .panel-title {
margin: 0 0 8px 0 !important;
font-size: 18px !important;
font-weight: 700 !important;
text-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
#hf-bookmarklet-panel .panel-subtitle {
margin: 0 !important;
font-size: 12px !important;
opacity: 0.9 !important;
}
#hf-bookmarklet-panel .panel-body {
padding: 20px !important;
}
#hf-bookmarklet-panel .hf-button {
width: 100% !important;
padding: 12px 16px !important;
margin: 8px 0 !important;
border: none !important;
border-radius: 10px !important;
font-weight: 600 !important;
font-size: 13px !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
gap: 8px !important;
position: relative !important;
overflow: hidden !important;
}
#hf-bookmarklet-panel .hf-button:hover {
transform: translateY(-2px) !important;
box-shadow: 0 10px 20px rgba(0,0,0,0.2) !important;
}
#hf-bookmarklet-panel .hf-button:active {
transform: translateY(0) !important;
}
#hf-bookmarklet-panel .btn-primary {
background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
color: white !important;
}
#hf-bookmarklet-panel .btn-secondary {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%) !important;
color: white !important;
}
#hf-bookmarklet-panel .btn-warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%) !important;
color: white !important;
}
#hf-bookmarklet-panel .btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
color: white !important;
}
#hf-bookmarklet-panel .status-message {
margin-top: 16px !important;
padding: 12px !important;
border-radius: 8px !important;
font-size: 12px !important;
display: none !important;
animation: fadeIn 0.3s ease !important;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
#hf-bookmarklet-panel .status-success {
background: rgba(16, 185, 129, 0.2) !important;
border: 1px solid rgba(16, 185, 129, 0.5) !important;
}
#hf-bookmarklet-panel .status-error {
background: rgba(239, 68, 68, 0.2) !important;
border: 1px solid rgba(239, 68, 68, 0.5) !important;
}
#hf-bookmarklet-panel .close-btn {
position: absolute !important;
top: 15px !important;
right: 15px !important;
background: rgba(255, 255, 255, 0.2) !important;
border: none !important;
border-radius: 50% !important;
width: 30px !important;
height: 30px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
}
#hf-bookmarklet-panel .close-btn:hover {
background: rgba(255, 255, 255, 0.3) !important;
transform: rotate(90deg) !important;
}
#hf-bookmarklet-panel .minimize-btn {
position: absolute !important;
top: 15px !important;
right: 55px !important;
background: rgba(255, 255, 255, 0.2) !important;
border: none !important;
border-radius: 50% !important;
width: 30px !important;
height: 30px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
}
#hf-bookmarklet-panel.minimized {
height: 60px !important;
overflow: hidden !important;
}
#hf-bookmarklet-panel.minimized .panel-body {
display: none !important;
}
`;
document.head.appendChild(style);
}
getPanelHTML() {
const aiIcon = this.aiType === 'Qwen' ? '🤖' : (this.aiType === 'Zai' ? '🔧' : '💬');
return `
${this.aiType === 'Qwen' ? `
` : ''}
`;
}
setupEventListeners() {
// Adicionar atalhos de teclado
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch(e.key) {
case 'v':
if (e.shiftKey) {
e.preventDefault();
this.smartPaste();
}
break;
case 'c':
if (e.shiftKey) {
e.preventDefault();
this.extractResponse();
}
break;
}
}
});
// Detectar mudanças no DOM
const observer = new MutationObserver(() => {
this.checkForIncomingData();
});
observer.observe(document.body, { childList: true, subtree: true });
}
enhanceInterface() {
// Melhorar a interface do chat
const chatContainer = document.querySelector('div.h-full.overflow-y-auto');
if (chatContainer) {
chatContainer.style.scrollBehavior = 'smooth';
chatContainer.style.padding = '20px';
// Adicionar botões flutuantes
this.addFloatingButtons();
this.showStatus('Interface melhorada com sucesso! 🎉', 'success');
} else {
this.showStatus('Container do chat não encontrado', 'error');
}
}
addFloatingButtons() {
// Remover botões existentes
const existing = document.querySelectorAll('.hf-floating-btn');
existing.forEach(btn => btn.remove());
// Botão de rolagem para baixo
const scrollBtn = document.createElement('button');
scrollBtn.innerHTML = '↓';
scrollBtn.className = 'hf-floating-btn';
scrollBtn.style.cssText = `
position: fixed !important;
bottom: 20px !important;
right: 20px !important;
width: 50px !important;
height: 50px !important;
border-radius: 50% !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
border: none !important;
cursor: pointer !important;
font-size: 20px !important;
z-index: 2147483646 !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
transition: all 0.3s ease !important;
`;
scrollBtn.onclick = () => {
const chat = document.querySelector('div.h-full.overflow-y-auto');
if (chat) chat.scrollTop = chat.scrollHeight;
};
document.body.appendChild(scrollBtn);
// Botão de foco no input
const focusBtn = document.createElement('button');
focusBtn.innerHTML = '✏️';
focusBtn.className = 'hf-floating-btn';
focusBtn.style.cssText = scrollBtn.style.cssText.replace('bottom: 20px', 'bottom: 80px');
focusBtn.onclick = () => {
const input = document.querySelector('textarea, input[type="text"]');
if (input) {
input.focus();
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
};
document.body.appendChild(focusBtn);
// Efeito hover
[scrollBtn, focusBtn].forEach(btn => {
btn.addEventListener('mouseenter', () => {
btn.style.transform = 'scale(1.1) rotate(5deg)';
});
btn.addEventListener('mouseleave', () => {
btn.style.transform = 'scale(1) rotate(0deg)';
});
});
}
async smartPaste() {
try {
const text = await navigator.clipboard.readText();
if (!text) {
this.showStatus('Área de transferência vazia', 'error');
return;
}
const textarea = await this.findInputField();
if (textarea) {
this.fillInputField(textarea, text);
this.showStatus('Texto colado com sucesso! 📋', 'success');
} else {
this.showStatus('Campo de entrada não encontrado', 'error');
}
} catch (error) {
console.error('Erro ao colar:', error);
this.showStatus('Erro ao acessar área de transferência', 'error');
}
}
async findInputField() {
const selectors = [
'textarea[data-testid="chat-input"]',
'textarea[placeholder*="Message"]',
'textarea[placeholder*="message"]',
'textarea[aria-label*="Message"]',
'textarea.w-full',
'textarea',
'input[type="text"]',
'[contenteditable="true"]'
];
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) return element;
}
return null;
}
fillInputField(input, text) {
if (input.tagName.toLowerCase() === 'textarea' || input.tagName.toLowerCase() === 'input') {
input.value = text;
input.focus();
// Disparar eventos
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
// Auto-ajustar altura se for textarea
if (input.tagName.toLowerCase() === 'textarea') {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 300) + 'px';
}
} else {
// Content editable
input.textContent = text;
input.focus();
}
}
extractResponse() {
const messages = this.findMessages();
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
const text = this.extractTextFromElement(lastMessage);
if (text && text.length > 10) {
navigator.clipboard.writeText(text).then(() => {
this.showStatus('Resposta copiada! 📋', 'success');
}).catch(() => {
// Fallback
this.fallbackCopy(text);
this.showStatus('Resposta copiada (fallback)!', 'success');
});
} else {
this.showStatus('Nenhum texto válido encontrado', 'error');
}
} else {
this.showStatus('Nenhuma mensagem encontrada', 'error');
}
}
findMessages() {
const selectors = [
'[data-testid="conversation-turn"]',
'.prose',
'.message-content',
'.markdown-body',
'[class*="message"]',
'[class*="response"]',
'[class*="turn"]'
];
for (const selector of selectors) {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) return elements;
}
// Busca genérica
const allElements = document.querySelectorAll('*');
const messages = [];
for (const element of allElements) {
const text = this.extractTextFromElement(element);
if (text && text.length > 200) {
const rect = element.getBoundingClientRect();
if (rect.width > 300 && rect.height > 100) {
messages.push(element);
}
}
}
return messages.slice(-3); // Últimas 3 mensagens
}
extractTextFromElement(element) {
if (element.textContent) {
return element.textContent.trim();
} else if (element.innerText) {
return element.innerText.trim();
} else if (element.innerHTML) {
return element.innerHTML.replace(/<[^>]*>/g, '').trim();
}
return '';
}
fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
sendToZai() {
const messages = this.findMessages();
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
const text = this.extractTextFromElement(lastMessage);
if (text && text.length > 10) {
localStorage.setItem('hf_qwen_to_zai', text);
window.open('https://huggingface.co/zai-org/GLM-4.6', '_blank');
this.showStatus('Enviado para ZAI! Nova aba aberta ➡️', 'success');
} else {
this.showStatus('Não há conteúdo para enviar', 'error');
}
} else {
this.showStatus('Nenhuma mensagem encontrada', 'error');
}
}
checkForIncomingData() {
if (this.aiType !== 'Zai') return;
const incomingData = localStorage.getItem('hf_qwen_to_zai');
if (incomingData) {
setTimeout(async () => {
const input = await this.findInputField();
if (input) {
this.fillInputField(input, incomingData);
this.showStatus('Dados do Qwen recebidos e injetados! 📥', 'success');
localStorage.removeItem('hf_qwen_to_zai');
}
}, 2000);
}
}
fullScreenMode() {
const chatContainer = document.querySelector('div.h-full.overflow-y-auto');
const messageBox = chatContainer?.parentElement;
const mainWrapper = messageBox?.parentElement;
if (!chatContainer || !messageBox || !mainWrapper) {
this.showStatus('Estrutura do chat não encontrada', 'error');
return;
}
// Salvar estilos originais
const originalStyles = {
main: mainWrapper.style.cssText,
message: messageBox.style.cssText,
chat: chatContainer.style.cssText
};
// Aplicar tela cheia
mainWrapper.style.cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
display: flex !important;
flex-direction: column !important;
z-index: 2147483645 !important;
background: #f9fafb !important;
padding: 20px !important;
box-sizing: border-box !important;
`;
messageBox.style.cssText = `
flex-grow: 1 !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
`;
chatContainer.style.cssText = `
flex-grow: 1 !important;
overflow-y: auto !important;
position: relative !important;
`;
// Botão para sair da tela cheia
const exitBtn = document.createElement('button');
exitBtn.textContent = '✖ Sair Tela Cheia';
exitBtn.style.cssText = `
position: fixed !important;
top: 25px !important;
right: 25px !important;
z-index: 2147483646 !important;
background: #ef4444 !important;
color: white !important;
border: none !important;
border-radius: 8px !important;
padding: 12px 20px !important;
font-weight: bold !important;
cursor: pointer !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
`;
exitBtn.onclick = () => {
mainWrapper.style.cssText = originalStyles.main;
messageBox.style.cssText = originalStyles.message;
chatContainer.style.cssText = originalStyles.chat;
exitBtn.remove();
this.showStatus('Modo tela cheia desativado', 'success');
};
document.body.appendChild(exitBtn);
this.showStatus('Modo tela cheia ativado! 🖥️', 'success');
}
resetPage() {
if (confirm('Tem certeza? Isso recarregará a página e perderá dados não salvos.')) {
localStorage.removeItem('hf_qwen_to_zai');
location.reload();
}
}
toggleMinimize() {
if (this.currentPanel) {
this.currentPanel.classList.toggle('minimized');
}
}
removePanel() {
if (this.currentPanel) {
this.currentPanel.remove();
this.isInjected = false;
}
// Remover botões flutuantes
document.querySelectorAll('.hf-floating-btn').forEach(btn => btn.remove());
}
showStatus(message, type = 'info') {
const statusEl = document.getElementById('hf-status');
if (!statusEl) return;
statusEl.textContent = message;
statusEl.className = `status-message status-${type}`;
statusEl.style.display = 'block';
setTimeout(() => {
statusEl.style.display = 'none';
}, 4000);
}
async autoPasteFromClipboard() {
// Verificar automaticamente se há algo para colar
try {
const text = await navigator.clipboard.readText();
if (text && text.length > 100 && this.aiType === 'Zai') {
// Provavelmente veio do Qwen
setTimeout(() => this.smartPaste(), 1000);
}
} catch (e) {
// Silencioso
}
}
}
// Instância global
const bookmarkletManager = new BookmarkletManager();
// Função principal para ser chamada pelo bookmarklet
function initHFBookmarklet() {
// Verificar se estamos no Hugging Face
if (!window.location.href.includes('huggingface.co')) {
window.open('https://huggingface.co/chat/', '_blank');
return;
}
bookmarkletManager.injectMainPanel();
}
// Auto-iniciar se a página já estiver carregada
if (document.readyState === 'complete') {
setTimeout(initHFBookmarklet, 1000);
} else {
window.addEventListener('load', () => {
setTimeout(initHFBookmarklet, 1000);
});
}
// Expor para uso manual
window.initHFBookmarklet = initHFBookmarklet;
window.bookmarkletManager = bookmarkletManager;