Settings
Desktop bookmarklet
Drag this link to your bookmarks bar, or copy the bookmarklet URL below into a new bookmark. When viewing a live ChatGPT chat, click it to send the chat here.
If dragging saves this Settings page instead, create a bookmark manually and paste the copied text into the bookmark URL field.
Show source
(async function(){const APP="https://chatxport.lovable.app";const delay=ms=>new Promise(res=>setTimeout(res,ms));function banner(t){let b=document.getElementById('__finalizer_banner');if(!b){b=document.createElement('div');b.id='__finalizer_banner';b.style.cssText='position:fixed;top:12px;right:12px;z-index:2147483647;background:#111;color:#fff;padding:10px 14px;border-radius:8px;font:14px system-ui;box-shadow:0 6px 24px rgba(0,0,0,.3);max-width:360px';document.body.appendChild(b);}b.textContent=t;return b;}function done(t,ms){const b=banner(t);setTimeout(()=>b&&b.remove(),ms||6000);}async function inlineImg(src){if(!src||src.startsWith('data:'))return src;try{const r=await fetch(src,{mode:'cors',credentials:'omit'});if(!r.ok)throw 0;const b=await r.blob();if(!b.type.startsWith('image/'))throw 0;return await new Promise((res,rej)=>{const fr=new FileReader();fr.onload=()=>res(fr.result);fr.onerror=rej;fr.readAsDataURL(b);});}catch(e){try{return await new Promise((res,rej)=>{const im=new Image();im.crossOrigin='anonymous';im.onload=()=>{try{const c=document.createElement('canvas');c.width=im.naturalWidth;c.height=im.naturalHeight;c.getContext('2d').drawImage(im,0,0);res(c.toDataURL('image/png'));}catch(e2){rej(e2);}};im.onerror=rej;im.src=src;});}catch(e2){return src;}}}function extractText(node,role){if(role==='user'){const candidates=[node.querySelector('[data-message-content]'),node.querySelector('.whitespace-pre-wrap'),node.querySelector('.text-message-content'),node.querySelector('[class*="user-message"]'),node.querySelector('.markdown, .prose')];for(const c of candidates){if(c){const t=(c.innerText||c.textContent||'').trim();if(t)return t;}}const clone=node.cloneNode(true);clone.querySelectorAll('button, [role="button"], svg, .sr-only').forEach(el=>el.remove());return (clone.innerText||clone.textContent||'').trim();}const md=node.querySelector('.markdown, .prose, [data-message-content]');if(md){const t=(md.innerText||md.textContent||'').trim();if(t)return t;}return (node.innerText||node.textContent||'').trim();}try{banner('📥 Finalizer: loading full chat…');let lastHeight=0;for(let i=0;i<60;i++){window.scrollTo(0,document.body.scrollHeight);await delay(300);const newHeight=document.body.scrollHeight;if(newHeight===lastHeight)break;lastHeight=newHeight;}window.scrollTo(0,0);await delay(300);const nodes=[...document.querySelectorAll('[data-message-author-role]')];if(!nodes.length){done('⚠️ Finalizer: no ChatGPT messages found on this page.');return;}banner('📥 Finalizer: extracting '+nodes.length+' messages…');const messages=[];let totalImgs=0;for(const node of nodes){const role=node.getAttribute('data-message-author-role')||'assistant';let text='';for(let i=0;i<8;i++){text=extractText(node,role);if(text)break;await delay(150);}const rawImgs=[...node.querySelectorAll('img')].map(img=>img.currentSrc||img.src).filter(s=>s&&!s.startsWith('data:image/svg'));totalImgs+=rawImgs.length;if(text||rawImgs.length)messages.push({role,text,_rawImgs:rawImgs});}if(!messages.length){done('⚠️ Finalizer: found message containers but no readable text.');return;}if(totalImgs){banner('🖼️ Finalizer: inlining '+totalImgs+' image(s)…');for(const m of messages){m.images=await Promise.all(m._rawImgs.map(inlineImg));delete m._rawImgs;}}else{for(const m of messages){m.images=[];delete m._rawImgs;}}const userCount=messages.filter(m=>m.role==='user').length;const asstCount=messages.filter(m=>m.role==='assistant').length;const title=(document.title||'ChatGPT chat').replace(/^ChatGPT\s*[-—]?\s*/,'')||'ChatGPT chat';const payload={title,source_url:location.href,messages};banner('📤 Finalizer: uploading '+userCount+'u+'+asstCount+'a…');try{const res=await fetch(APP+'/api/public/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});const out=await res.json().catch(()=>({}));if(!res.ok||!out.id)throw new Error(out.error||('Import failed: '+res.status));banner('✅ Finalizer: opening Compose…');const w=window.open(APP+'/compose?import='+encodeURIComponent(out.id),'_blank');if(!w)done('✅ Finalizer: imported. Open '+APP+'/compose?import='+out.id,15000);else setTimeout(()=>document.getElementById('__finalizer_banner')?.remove(),2500);}catch(err){const json=JSON.stringify(payload);const blob=new Blob([json],{type:'application/json'});const fallback=URL.createObjectURL(blob);window.open(fallback,'_blank');done('⚠️ Finalizer: upload failed ('+(err&&err.message?err.message:err)+'). Opened JSON fallback.',12000);}}catch(err){done('⚠️ Finalizer error: '+(err&&err.message?err.message:err),10000);}})();iOS Shortcut (Safari)
On iPhone Safari, the share-sheet → "Run JavaScript" pattern works. Create a Shortcut with:
- "Get Current Web Page from Safari Web View"
- "Run JavaScript on Web Page" — paste the snippet below
- "Open URLs" — use the JavaScript Result
- Toggle "Show in Share Sheet" in the Shortcut settings
Show source
(function(){const APP="https://chatxport.lovable.app";let finished=false;function done(v){if(finished)return;finished=true;completion(v);}const delay=ms=>new Promise(r=>setTimeout(r,ms));setTimeout(()=>done('ERR:TIMEOUT_TRY_AGAIN_FROM_CHATGPT_TAB'),25000);async function inlineImg(src){if(!src||src.startsWith('data:'))return src;try{const r=await fetch(src,{mode:'cors',credentials:'omit'});if(!r.ok)throw 0;const b=await r.blob();if(!b.type.startsWith('image/'))throw 0;return await new Promise((res,rej)=>{const fr=new FileReader();fr.onload=()=>res(fr.result);fr.onerror=rej;fr.readAsDataURL(b);});}catch(e){try{return await new Promise((res,rej)=>{const im=new Image();im.crossOrigin='anonymous';im.onload=()=>{try{const c=document.createElement('canvas');c.width=im.naturalWidth;c.height=im.naturalHeight;c.getContext('2d').drawImage(im,0,0);res(c.toDataURL('image/png'));}catch(e2){rej(e2);}};im.onerror=rej;im.src=src;});}catch(e2){return src;}}}function extractText(node,role){if(role==='user'){const candidates=[node.querySelector('[data-message-content]'),node.querySelector('.whitespace-pre-wrap'),node.querySelector('.text-message-content'),node.querySelector('[class*="user-message"]'),node.querySelector('.markdown, .prose')];for(const c of candidates){if(c){const t=(c.innerText||c.textContent||'').trim();if(t)return t;}}const clone=node.cloneNode(true);clone.querySelectorAll('button, [role="button"], svg, .sr-only').forEach(el=>el.remove());return (clone.innerText||clone.textContent||'').trim();}const md=node.querySelector('.markdown, .prose, [data-message-content]');if(md){const t=(md.innerText||md.textContent||'').trim();if(t)return t;}return (node.innerText||node.textContent||'').trim();}(async function(){try{let nodes=[...document.querySelectorAll('[data-message-author-role]')];if(!nodes.length){window.scrollTo(0,document.body.scrollHeight);await delay(500);nodes=[...document.querySelectorAll('[data-message-author-role]')];}if(!nodes.length){done(APP+'/import?receive=1&error=NO_MESSAGES');return;}const messages=[];for(const node of nodes){const role=node.getAttribute('data-message-author-role')||'assistant';const text=extractText(node,role);const rawImgs=[...node.querySelectorAll('img')].map(img=>img.currentSrc||img.src).filter(s=>s&&!s.startsWith('data:image/svg'));const images=rawImgs.length?await Promise.all(rawImgs.map(inlineImg)):[];if(text||images.length)messages.push({role,text,images});}if(!messages.length){done(APP+'/import?receive=1&error=NO_MESSAGES');return;}const title=(document.title||'ChatGPT chat').replace(/^ChatGPT\s*[-—]?\s*/,'')||'ChatGPT chat';const payload={title,source_url:location.href,messages};const res=await fetch(APP+'/api/public/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});const out=await res.json().catch(()=>({}));if(!res.ok||!out.id)throw new Error(out.error||('Import failed: '+res.status));done(APP+'/compose?import='+encodeURIComponent(out.id));}catch(err){done(APP+'/import?receive=1&error='+encodeURIComponent(err&&err.message?err.message:String(err)));}})();})();Provider
OpenRouter is configured via the OPENROUTER_API_KEY server secret.
All model calls happen server-side; the key is never exposed to the browser.