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})();