Lovable Credit Manager - Unlimited AI Interactions

Gerencia interações com Lovable sem consumir créditos

Size

24.1 KB

Version

1.1.3

Created

Mar 6, 2026

Updated

about 1 month ago

1// ==UserScript==
2// @name		Lovable Credit Manager - Unlimited AI Interactions
3// @description		Gerencia interações com Lovable sem consumir créditos
4// @version		1.1.3
5// @match		https://*.lovable.dev/*
6// @icon		https://lovable.dev/favicon.svg
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.xmlHttpRequest
10// @run-at		document-start
11// ==/UserScript==
12(function() {
13    'use strict';
14
15    console.log('🚀 Lovable Credit Manager iniciado');
16
17    // Armazenar créditos originais e estado
18    let originalCredits = null;
19    let creditsFrozen = false;
20
21    // Função para inicializar
22    async function init() {
23        console.log('Inicializando Lovable Credit Manager...');
24        
25        // Aguardar o carregamento da página
26        await waitForElement('body');
27        
28        // Carregar estado salvo
29        originalCredits = await GM.getValue('originalCredits', null);
30        creditsFrozen = await GM.getValue('creditsFrozen', false);
31        
32        console.log('💾 Estado carregado - Créditos:', originalCredits, 'Congelado:', creditsFrozen);
33        
34        // Interceptar requisições ANTES de qualquer outra coisa
35        interceptFetch();
36        interceptXHR();
37        
38        // Monitorar mudanças de créditos no DOM
39        monitorCredits();
40        
41        // Criar interface com chat
42        createUI();
43        
44        console.log('✅ Lovable Credit Manager ativo');
45    }
46
47    // Aguardar elemento aparecer
48    function waitForElement(selector, timeout = 10000) {
49        return new Promise((resolve, reject) => {
50            if (document.querySelector(selector)) {
51                return resolve(document.querySelector(selector));
52            }
53
54            const observer = new MutationObserver(() => {
55                if (document.querySelector(selector)) {
56                    observer.disconnect();
57                    resolve(document.querySelector(selector));
58                }
59            });
60
61            observer.observe(document.body, {
62                childList: true,
63                subtree: true
64            });
65
66            setTimeout(() => {
67                observer.disconnect();
68                reject(new Error('Timeout waiting for element: ' + selector));
69            }, timeout);
70        });
71    }
72
73    // Interceptar fetch - VERSÃO MELHORADA
74    function interceptFetch() {
75        const originalFetch = window.fetch;
76        
77        window.fetch = async function(...args) {
78            const [url, options] = args;
79            
80            // Log de todas as requisições para debug
81            if (url.includes('lovable') || url.includes('api')) {
82                console.log('🔍 Fetch detectado:', url);
83            }
84            
85            // Executar a requisição original
86            const response = await originalFetch(...args);
87            
88            // Verificar se é uma requisição relacionada a créditos
89            if (url.includes('api.lovable.dev') || url.includes('lovable.dev/api') || 
90                url.includes('credit') || url.includes('usage') || url.includes('user')) {
91                
92                console.log('💳 Requisição de API Lovable:', url);
93                
94                // Clonar a resposta para poder ler o corpo
95                const clonedResponse = response.clone();
96                
97                try {
98                    const contentType = response.headers.get('content-type');
99                    if (contentType && contentType.includes('application/json')) {
100                        const data = await clonedResponse.json();
101                        console.log('📊 Resposta da API:', data);
102                        
103                        // Procurar por campos de créditos em qualquer nível do objeto
104                        const creditFields = findCreditFields(data);
105                        
106                        if (creditFields.length > 0) {
107                            console.log('💰 Campos de créditos encontrados:', creditFields);
108                            
109                            // Salvar créditos originais se ainda não temos
110                            if (originalCredits === null) {
111                                originalCredits = creditFields[0].value;
112                                await GM.setValue('originalCredits', originalCredits);
113                                console.log('💾 Créditos originais salvos:', originalCredits);
114                                updateCreditDisplay();
115                            }
116                            
117                            // Se os créditos estão congelados, modificar a resposta
118                            if (creditsFrozen) {
119                                const modifiedData = modifyCreditsInObject(data, originalCredits);
120                                console.log('✨ Resposta modificada - Créditos mantidos em:', originalCredits);
121                                
122                                // Retornar resposta modificada
123                                return new Response(JSON.stringify(modifiedData), {
124                                    status: response.status,
125                                    statusText: response.statusText,
126                                    headers: response.headers
127                                });
128                            }
129                        }
130                    }
131                } catch (e) {
132                    console.log('⚠️ Erro ao processar resposta:', e);
133                }
134            }
135            
136            return response;
137        };
138        
139        console.log('✅ Fetch interceptado');
140    }
141
142    // Função para encontrar campos de créditos em objetos aninhados
143    function findCreditFields(obj, path = '') {
144        const creditFields = [];
145        const creditKeywords = ['credit', 'credits', 'creditsRemaining', 'creditsLeft', 'balance', 'remaining'];
146        
147        function search(o, p) {
148            if (typeof o !== 'object' || o === null) return;
149            
150            for (const key in o) {
151                const currentPath = p ? `${p}.${key}` : key;
152                const lowerKey = key.toLowerCase();
153                
154                // Verificar se é um campo de créditos
155                if (creditKeywords.some(keyword => lowerKey.includes(keyword.toLowerCase()))) {
156                    if (typeof o[key] === 'number') {
157                        creditFields.push({ path: currentPath, key: key, value: o[key] });
158                    }
159                }
160                
161                // Buscar recursivamente
162                if (typeof o[key] === 'object') {
163                    search(o[key], currentPath);
164                }
165            }
166        }
167        
168        search(obj, path);
169        return creditFields;
170    }
171
172    // Função para modificar créditos em objetos aninhados
173    function modifyCreditsInObject(obj, newValue) {
174        const modified = JSON.parse(JSON.stringify(obj)); // Deep clone
175        const creditKeywords = ['credit', 'credits', 'creditsRemaining', 'creditsLeft', 'balance', 'remaining'];
176        
177        function modify(o) {
178            if (typeof o !== 'object' || o === null) return;
179            
180            for (const key in o) {
181                const lowerKey = key.toLowerCase();
182                
183                // Modificar se for um campo de créditos
184                if (creditKeywords.some(keyword => lowerKey.includes(keyword.toLowerCase()))) {
185                    if (typeof o[key] === 'number') {
186                        o[key] = newValue;
187                    }
188                }
189                
190                // Modificar recursivamente
191                if (typeof o[key] === 'object') {
192                    modify(o[key]);
193                }
194            }
195        }
196        
197        modify(modified);
198        return modified;
199    }
200
201    // Interceptar XMLHttpRequest
202    function interceptXHR() {
203        const originalOpen = XMLHttpRequest.prototype.open;
204        const originalSend = XMLHttpRequest.prototype.send;
205        
206        XMLHttpRequest.prototype.open = function(method, url, ...rest) {
207            this._url = url;
208            this._method = method;
209            return originalOpen.call(this, method, url, ...rest);
210        };
211        
212        XMLHttpRequest.prototype.send = function(...args) {
213            if (this._url && (this._url.includes('api.lovable.dev') || this._url.includes('lovable.dev/api') || 
214                this._url.includes('credit') || this._url.includes('usage'))) {
215                
216                console.log('🔍 XHR interceptado:', this._url);
217                
218                const originalOnReadyStateChange = this.onreadystatechange;
219                
220                this.addEventListener('readystatechange', async function() {
221                    if (this.readyState === 4 && this.status === 200) {
222                        try {
223                            const data = JSON.parse(this.responseText);
224                            console.log('📊 Dados XHR:', data);
225                            
226                            const creditFields = findCreditFields(data);
227                            
228                            if (creditFields.length > 0 && originalCredits === null) {
229                                originalCredits = creditFields[0].value;
230                                await GM.setValue('originalCredits', originalCredits);
231                                console.log('💰 Créditos originais salvos (XHR):', originalCredits);
232                                updateCreditDisplay();
233                            }
234                        } catch (e) {
235                            console.log('⚠️ Erro ao processar XHR:', e);
236                        }
237                    }
238                });
239            }
240            
241            return originalSend.call(this, ...args);
242        };
243        
244        console.log('✅ XMLHttpRequest interceptado');
245    }
246
247    // Monitorar mudanças de créditos no DOM
248    function monitorCredits() {
249        const observer = new MutationObserver(async (mutations) => {
250            if (!creditsFrozen) return;
251            
252            for (const mutation of mutations) {
253                if (mutation.type === 'childList' || mutation.type === 'characterData') {
254                    // Procurar por elementos que possam conter informações de créditos
255                    const creditElements = document.querySelectorAll('[class*="credit"], [class*="balance"], [data-testid*="credit"], [aria-label*="credit"]');
256                    
257                    for (const element of creditElements) {
258                        const text = element.textContent;
259                        const creditMatch = text.match(/(\d+)\s*(credit|crédito|remaining)/i);
260                        
261                        if (creditMatch) {
262                            const currentCredits = parseInt(creditMatch[1]);
263                            
264                            if (originalCredits !== null && currentCredits < originalCredits) {
265                                // Restaurar os créditos no DOM
266                                console.log('🔄 Restaurando créditos no DOM de', currentCredits, 'para', originalCredits);
267                                element.textContent = text.replace(creditMatch[1], originalCredits.toString());
268                            }
269                        }
270                    }
271                }
272            }
273        });
274        
275        observer.observe(document.body, {
276            childList: true,
277            subtree: true,
278            characterData: true
279        });
280        
281        console.log('✅ Monitor de créditos ativo');
282    }
283
284    // Atualizar display de créditos
285    function updateCreditDisplay() {
286        const creditsElement = document.getElementById('lcm-credits');
287        if (creditsElement && originalCredits !== null) {
288            creditsElement.textContent = originalCredits;
289        }
290    }
291
292    // Enviar mensagem para Lovable
293    async function sendMessageToLovable(message) {
294        try {
295            console.log('📤 Enviando mensagem para Lovable:', message);
296            
297            // Adicionar mensagem ao histórico do chat
298            addMessageToChat('Você', message, 'user');
299            
300            // Encontrar o input do chat da Lovable
301            const chatInput = document.querySelector('#chatinput .ProseMirror');
302            if (!chatInput) {
303                throw new Error('Input do chat não encontrado');
304            }
305            
306            // Inserir o texto no input
307            chatInput.focus();
308            chatInput.innerHTML = `<p>${message}</p>`;
309            
310            // Disparar eventos para simular digitação
311            chatInput.dispatchEvent(new Event('input', { bubbles: true }));
312            chatInput.dispatchEvent(new Event('change', { bubbles: true }));
313            
314            // Aguardar um pouco para garantir que o texto foi inserido
315            await new Promise(resolve => setTimeout(resolve, 500));
316            
317            // Encontrar e clicar no botão de enviar
318            const submitButton = document.querySelector('#chat-input button[type="submit"]');
319            if (!submitButton) {
320                // Tentar encontrar o botão de outra forma
321                const buttons = document.querySelectorAll('#chat-input button');
322                for (const btn of buttons) {
323                    if (btn.textContent.includes('Send') || btn.querySelector('svg')) {
324                        btn.click();
325                        console.log('✅ Mensagem enviada!');
326                        addMessageToChat('Lovable', 'Processando sua solicitação...', 'assistant');
327                        return;
328                    }
329                }
330                throw new Error('Botão de enviar não encontrado');
331            }
332            
333            submitButton.click();
334            console.log('✅ Mensagem enviada!');
335            addMessageToChat('Lovable', 'Processando sua solicitação...', 'assistant');
336            
337        } catch (error) {
338            console.error('❌ Erro ao enviar mensagem:', error);
339            addMessageToChat('Sistema', 'Erro ao enviar mensagem: ' + error.message, 'error');
340        }
341    }
342
343    // Adicionar mensagem ao chat
344    function addMessageToChat(sender, message, type) {
345        const chatMessages = document.getElementById('lcm-chat-messages');
346        if (!chatMessages) return;
347        
348        const messageDiv = document.createElement('div');
349        messageDiv.style.cssText = `
350            margin-bottom: 12px;
351            padding: 10px;
352            border-radius: 8px;
353            font-size: 13px;
354            line-height: 1.4;
355            ${type === 'user' ? 'background: rgba(102, 126, 234, 0.2); margin-left: 20px;' : ''}
356            ${type === 'assistant' ? 'background: rgba(118, 75, 162, 0.2); margin-right: 20px;' : ''}
357            ${type === 'error' ? 'background: rgba(239, 68, 68, 0.2); border: 1px solid rgba(239, 68, 68, 0.5);' : ''}
358        `;
359        
360        messageDiv.innerHTML = `
361            <div style="font-weight: bold; margin-bottom: 4px; font-size: 11px; opacity: 0.8;">${sender}</div>
362            <div style="white-space: pre-wrap;">${message}</div>
363        `;
364        
365        chatMessages.appendChild(messageDiv);
366        chatMessages.scrollTop = chatMessages.scrollHeight;
367    }
368
369    // Criar interface de usuário
370    function createUI() {
371        const container = document.createElement('div');
372        container.id = 'lovable-credit-manager';
373        container.style.cssText = `
374            position: fixed;
375            bottom: 20px;
376            right: 20px;
377            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
378            color: white;
379            border-radius: 16px;
380            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
381            z-index: 999999;
382            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
383            width: 400px;
384            max-height: 600px;
385            display: flex;
386            flex-direction: column;
387            backdrop-filter: blur(10px);
388        `;
389        
390        container.innerHTML = `
391            <div style="padding: 15px 20px; border-bottom: 1px solid rgba(255,255,255,0.2);">
392                <div style="display: flex; align-items: center; justify-content: space-between;">
393                    <div style="display: flex; align-items: center; gap: 8px;">
394                        <span style="font-size: 20px;">💎</span>
395                        <strong style="font-size: 14px;">Lovable Credit Manager</strong>
396                    </div>
397                    <div style="display: flex; gap: 8px;">
398                        <button id="lcm-toggle-chat" style="background: rgba(255,255,255,0.2); border: none; color: white; padding: 4px 8px; border-radius: 6px; cursor: pointer; font-size: 12px;">
399                            Chat
400                        </button>
401                        <button id="lcm-minimize" style="background: rgba(255,255,255,0.2); border: none; color: white; padding: 4px 8px; border-radius: 6px; cursor: pointer; font-size: 12px;">
402                            _
403                        </button>
404                    </div>
405                </div>
406            </div>
407            
408            <div id="lcm-content" style="padding: 15px 20px;">
409                <div style="font-size: 12px; opacity: 0.9; margin-bottom: 8px;">
410                    Status: <span id="lcm-status" style="color: #4ade80; font-weight: bold;">✓ Ativo</span>
411                </div>
412                <div style="font-size: 12px; opacity: 0.9; margin-bottom: 12px;">
413                    Créditos: <span id="lcm-credits" style="font-weight: bold;">Detectando...</span>
414                </div>
415                <button id="lcm-freeze-toggle" style="
416                    width: 100%;
417                    padding: 10px;
418                    background: rgba(255,255,255,0.2);
419                    border: 2px solid rgba(255,255,255,0.3);
420                    color: white;
421                    border-radius: 8px;
422                    cursor: pointer;
423                    font-size: 13px;
424                    font-weight: bold;
425                    transition: all 0.2s;
426                ">
427                    🔒 Congelar Créditos
428                </button>
429                <div style="font-size: 11px; opacity: 0.7; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.2);">
430                    Clique para ativar/desativar o congelamento
431                </div>
432            </div>
433            
434            <div id="lcm-chat-panel" style="display: none; flex-direction: column; flex: 1; min-height: 0;">
435                <div id="lcm-chat-messages" style="
436                    flex: 1;
437                    overflow-y: auto;
438                    padding: 15px;
439                    background: rgba(0,0,0,0.2);
440                    min-height: 200px;
441                    max-height: 400px;
442                ">
443                    <div style="font-size: 12px; opacity: 0.7; text-align: center; padding: 20px;">
444                        Digite sua mensagem abaixo para interagir com a Lovable através da extensão
445                    </div>
446                </div>
447                <div style="padding: 15px; border-top: 1px solid rgba(255,255,255,0.2);">
448                    <textarea id="lcm-chat-input" placeholder="Digite sua mensagem para a Lovable..." style="
449                        width: 100%;
450                        min-height: 80px;
451                        padding: 10px;
452                        border: 1px solid rgba(255,255,255,0.3);
453                        border-radius: 8px;
454                        background: rgba(255,255,255,0.1);
455                        color: white;
456                        font-size: 13px;
457                        font-family: inherit;
458                        resize: vertical;
459                        margin-bottom: 8px;
460                    "></textarea>
461                    <button id="lcm-send-btn" style="
462                        width: 100%;
463                        padding: 10px;
464                        background: rgba(255,255,255,0.3);
465                        border: none;
466                        color: white;
467                        border-radius: 8px;
468                        cursor: pointer;
469                        font-size: 13px;
470                        font-weight: bold;
471                        transition: all 0.2s;
472                    ">
473                        📤 Enviar para Lovable
474                    </button>
475                </div>
476            </div>
477        `;
478        
479        document.body.appendChild(container);
480        
481        // Funcionalidade de minimizar
482        const minimizeBtn = document.getElementById('lcm-minimize');
483        const content = document.getElementById('lcm-content');
484        const chatPanel = document.getElementById('lcm-chat-panel');
485        let isMinimized = false;
486        
487        minimizeBtn.addEventListener('click', () => {
488            isMinimized = !isMinimized;
489            if (isMinimized) {
490                content.style.display = 'none';
491                chatPanel.style.display = 'none';
492                container.style.width = 'auto';
493                container.style.maxHeight = 'auto';
494                minimizeBtn.textContent = '□';
495            } else {
496                const chatVisible = chatPanel.style.display === 'flex';
497                content.style.display = chatVisible ? 'none' : 'block';
498                chatPanel.style.display = chatVisible ? 'flex' : 'none';
499                container.style.width = '400px';
500                container.style.maxHeight = '600px';
501                minimizeBtn.textContent = '_';
502            }
503        });
504        
505        // Toggle entre status e chat
506        const toggleChatBtn = document.getElementById('lcm-toggle-chat');
507        toggleChatBtn.addEventListener('click', () => {
508            const chatVisible = chatPanel.style.display === 'flex';
509            content.style.display = chatVisible ? 'block' : 'none';
510            chatPanel.style.display = chatVisible ? 'none' : 'flex';
511            toggleChatBtn.textContent = chatVisible ? 'Chat' : 'Status';
512        });
513        
514        // Botão de congelar créditos
515        const freezeBtn = document.getElementById('lcm-freeze-toggle');
516        updateFreezeButton();
517        
518        freezeBtn.addEventListener('click', async () => {
519            creditsFrozen = !creditsFrozen;
520            await GM.setValue('creditsFrozen', creditsFrozen);
521            updateFreezeButton();
522            console.log('🔒 Créditos', creditsFrozen ? 'congelados' : 'descongelados');
523        });
524        
525        function updateFreezeButton() {
526            if (creditsFrozen) {
527                freezeBtn.innerHTML = '🔓 Descongelar Créditos';
528                freezeBtn.style.background = 'rgba(74, 222, 128, 0.3)';
529                freezeBtn.style.borderColor = 'rgba(74, 222, 128, 0.5)';
530            } else {
531                freezeBtn.innerHTML = '🔒 Congelar Créditos';
532                freezeBtn.style.background = 'rgba(255,255,255,0.2)';
533                freezeBtn.style.borderColor = 'rgba(255,255,255,0.3)';
534            }
535        }
536        
537        // Botão de enviar mensagem
538        const sendBtn = document.getElementById('lcm-send-btn');
539        const chatInput = document.getElementById('lcm-chat-input');
540        
541        sendBtn.addEventListener('click', async () => {
542            const message = chatInput.value.trim();
543            if (!message) return;
544            
545            chatInput.value = '';
546            await sendMessageToLovable(message);
547        });
548        
549        // Enviar com Ctrl+Enter
550        chatInput.addEventListener('keydown', async (e) => {
551            if (e.ctrlKey && e.key === 'Enter') {
552                const message = chatInput.value.trim();
553                if (!message) return;
554                
555                chatInput.value = '';
556                await sendMessageToLovable(message);
557            }
558        });
559        
560        // Atualizar créditos na UI
561        setInterval(async () => {
562            const credits = await GM.getValue('originalCredits', null);
563            if (credits !== null) {
564                document.getElementById('lcm-credits').textContent = credits;
565            }
566        }, 1000);
567        
568        console.log('✅ Interface criada');
569    }
570
571    // Inicializar quando o DOM estiver pronto
572    if (document.readyState === 'loading') {
573        document.addEventListener('DOMContentLoaded', init);
574    } else {
575        init();
576    }
577
578})();