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(){var API="https://project--46b9dc8d-7de6-4411-a7b4-764c8aace83e.lovable.app";var UI="https://finalizer.ontollogic.ai";var RECOVERY_TPL="<!doctype html><meta charset=utf-8><title>Finalizer — upload failed</title><style>body{font:15px/1.5 system-ui;margin:0;padding:32px;background:#0b0b0c;color:#eee}h1{font-size:20px;margin:0 0 8px}h2{font-size:14px;text-transform:uppercase;letter-spacing:.08em;color:#aaa;margin:24px 0 8px}p{margin:8px 0;color:#ccc}.row{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}button,a.btn{background:#fff;color:#111;border:0;padding:10px 14px;border-radius:8px;font:600 14px system-ui;cursor:pointer;text-decoration:none;display:inline-block}button.ghost,a.btn.ghost{background:transparent;color:#fff;border:1px solid #444}.err{background:#2a1010;border:1px solid #b33;color:#fbb;padding:10px 12px;border-radius:6px;font:13px ui-monospace,Menlo,monospace;white-space:pre-wrap;word-break:break-word}textarea{width:100%;height:280px;background:#111;color:#ddd;border:1px solid #333;border-radius:6px;padding:10px;font:12px ui-monospace,Menlo,monospace;resize:vertical}.ok{color:#7fdca0}</style><h1>Finalizer — upload failed</h1><p>The chat was scraped successfully but the direct upload did not complete. Your data is below — nothing was lost.</p><h2>Error</h2><div class=err id=err>{{ERR}}</div><h2>Recovery</h2><div class=row><button id=copy>Copy JSON</button><button id=open>Open Import with payload</button><a class=\"btn ghost\" href=\"{{UI}}/import\" target=_blank>Open blank Import page</a><button class=ghost id=retry>Retry POST</button><button class=ghost id=dl>Download .json</button></div><p style=color:#888>Best option: click “Open Import with payload”. If the browser blocks it, copy the JSON, open Import, paste, and click Import.</p><h2>Payload</h2><textarea id=p readonly>{{TEXT}}</textarea><div id=status style=margin-top:8px></div><script id=payload type=\"application/json\">{{JSON}}</script><script>(function(){var API=\"{{API}}\";var UI=\"{{UI}}\";var payload=document.getElementById(\"payload\").textContent;document.getElementById(\"p\").value=payload||document.getElementById(\"p\").value;function setStatus(v){document.getElementById(\"status\").innerHTML=v;}document.getElementById(\"copy\").onclick=async function(){try{await navigator.clipboard.writeText(payload);setStatus('<span class=ok>Copied to clipboard.</span>');}catch(e){var t=document.getElementById(\"p\");t.select();document.execCommand(\"copy\");setStatus('<span class=ok>Copied via fallback.</span>');}};document.getElementById(\"open\").onclick=function(){var w=window.open(\"\",\"_blank\");if(!w){setStatus('<span style=color:#fbb>Popup blocked. Use Copy JSON instead.</span>');return;}w.name=\"FINALIZER_IMPORT_JSON:\"+payload;w.location.href=UI+\"/import?receive=1\";setStatus('<span class=ok>Opened Import with payload.</span>');};document.getElementById(\"dl\").onclick=function(){var b=new Blob([payload],{type:\"application/json\"});var a=document.createElement(\"a\");a.href=URL.createObjectURL(b);a.download=\"finalizer-chat.json\";a.click();};document.getElementById(\"retry\").onclick=async function(){var s=document.getElementById(\"status\");s.textContent=\"Retrying…\";try{var r=await fetch(API+\"/api/public/import\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:payload});var o=await r.json().catch(function(){return{};});if(!r.ok||!o.id)throw new Error(o.error||(\"HTTP \"+r.status));location.href=UI+\"/compose?import=\"+encodeURIComponent(o.id);}catch(e){s.innerHTML='<span style=color:#fbb>Retry failed: '+(e&&e.message?e.message:e)+'</span>';}};})();</script>";var handoffTarget='finalizer_'+Date.now();var handoffWin=null;try{handoffWin=window.open('about:blank',handoffTarget);if(handoffWin){handoffWin.document.open();handoffWin.document.write('<!doctype html><title>Finalizer</title><body style="font:16px system-ui;padding:32px">Preparing Finalizer import…</body>');handoffWin.document.close();}}catch(e){handoffWin=null;}var delay=function(ms){return new Promise(function(r){setTimeout(r,ms);});};function banner(t){var 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){var b=banner(t);setTimeout(function(){b&&b.remove();},ms||6000);}function openRecovery(payload,errMsg){var json=JSON.stringify(payload,null,2);var safeJson=json.replace(/<\//g,'<\\/');var safeErr=String(errMsg||'unknown error').replace(/[<>&]/g,function(c){return{'<':'&lt;','>':'&gt;','&':'&amp;'}[c];});var safeText=json.replace(/[<>&]/g,function(c){return{'<':'&lt;','>':'&gt;','&':'&amp;'}[c];});var html=RECOVERY_TPL.replace(/\{\{API\}\}/g,API).replace(/\{\{UI\}\}/g,UI).replace(/\{\{ERR\}\}/g,safeErr).replace(/\{\{TEXT\}\}/g,safeText).replace(/\{\{JSON\}\}/g,safeJson);var w=window.open('','_blank');if(w){w.document.open();w.document.write(html);w.document.close();}else{var blob=new Blob([html],{type:'text/html'});window.open(URL.createObjectURL(blob),'_blank');}}function openImport(payload){var json=JSON.stringify(payload);try{if(!handoffWin||handoffWin.closed)handoffWin=window.open('about:blank',handoffTarget);if(!handoffWin)throw new Error('Popup blocked');var f=document.createElement('form');f.method='POST';f.enctype='multipart/form-data';f.action=API+'/api/public/import';f.target=handoffTarget;f.style.display='none';var a=document.createElement('input');a.type='hidden';a.name='payload';a.value=json;var r=document.createElement('input');r.type='hidden';r.name='redirect_to';r.value=UI;f.appendChild(a);f.appendChild(r);document.body.appendChild(f);f.submit();setTimeout(function(){f.remove();},1000);return true;}catch(e){openRecovery(payload,e&&e.message?e.message:e);return false;}}function withTimeout(p,ms,fallback){return Promise.race([p,new Promise(function(r){setTimeout(function(){r(fallback);},ms);})]);}async function inlineImgRaw(src){if(!src||src.startsWith('data:'))return src;try{var r=await fetch(src,{mode:'cors',credentials:'omit'});if(!r.ok)throw 0;var b=await r.blob();if(!b.type.startsWith('image/'))throw 0;return await new Promise(function(res,rej){var fr=new FileReader();fr.onload=function(){res(fr.result);};fr.onerror=rej;fr.readAsDataURL(b);});}catch(e){try{return await new Promise(function(res,rej){var im=new Image();im.crossOrigin='anonymous';im.onload=function(){try{var 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 inlineImg(src){return withTimeout(inlineImgRaw(src),2500,src);}function extractText(node,role){if(role==='user'){var 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(var i=0;i<candidates.length;i++){var c=candidates[i];if(c){var t=(c.innerText||c.textContent||'').trim();if(t)return t;}}var clone=node.cloneNode(true);clone.querySelectorAll('button, [role="button"], svg, .sr-only').forEach(function(el){el.remove();});return (clone.innerText||clone.textContent||'').trim();}var md=node.querySelector('.markdown, .prose, [data-message-content]');if(md){var t2=(md.innerText||md.textContent||'').trim();if(t2)return t2;}return (node.innerText||node.textContent||'').trim();}try{banner('📥 Finalizer: loading full chat…');var lastHeight=0;for(var i=0;i<60;i++){window.scrollTo(0,document.body.scrollHeight);await delay(300);var newHeight=document.body.scrollHeight;if(newHeight===lastHeight)break;lastHeight=newHeight;}window.scrollTo(0,0);await delay(300);var nodes=[].slice.call(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…');var messages=[];var totalImgs=0;for(var ni=0;ni<nodes.length;ni++){var node=nodes[ni];var role=node.getAttribute('data-message-author-role')||'assistant';var text='';for(var ti=0;ti<8;ti++){text=extractText(node,role);if(text)break;await delay(150);}var rawImgs=[].slice.call(node.querySelectorAll('img')).map(function(img){return img.currentSrc||img.src;}).filter(function(s){return s&&!s.startsWith('data:image/svg');});totalImgs+=rawImgs.length;if(text||rawImgs.length)messages.push({role:role,text:text,_rawImgs:rawImgs});}if(!messages.length){done('⚠️ Finalizer: found message containers but no readable text.');return;}if(totalImgs){banner('🖼️ Finalizer: inlining '+totalImgs+' image(s)…');var allInline=Promise.all(messages.map(function(m){return Promise.all(m._rawImgs.map(inlineImg)).then(function(imgs){m.images=imgs;delete m._rawImgs;});}));await withTimeout(allInline,12000,null);for(var mi=0;mi<messages.length;mi++){if(!messages[mi].images){messages[mi].images=messages[mi]._rawImgs||[];delete messages[mi]._rawImgs;}}}else{for(var mj=0;mj<messages.length;mj++){messages[mj].images=[];delete messages[mj]._rawImgs;}}var userCount=messages.filter(function(m){return m.role==='user';}).length;var asstCount=messages.filter(function(m){return m.role==='assistant';}).length;var title=(document.title||'ChatGPT chat').replace(/^ChatGPT\s*[-—]?\s*/,'')||'ChatGPT chat';var payload={title:title,source_url:location.href,messages:messages};banner('📤 Finalizer: opening Import for '+userCount+'u+'+asstCount+'a…');if(openImport(payload)){done('✅ Finalizer: opened Import. It will finish the upload there.',6000);}else{done('⚠️ Finalizer: popup blocked. Opened recovery tab.',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:

  1. "Get Current Web Page from Safari Web View"
  2. "Run JavaScript on Web Page" — paste the snippet below
  3. "Open URLs" — use the JavaScript Result
  4. Toggle "Show in Share Sheet" in the Shortcut settings
Show source
(function(){var API="https://project--46b9dc8d-7de6-4411-a7b4-764c8aace83e.lovable.app";var UI="https://finalizer.ontollogic.ai";var finished=false;function done(v){if(finished)return;finished=true;completion(v);}var delay=function(ms){return new Promise(function(r){setTimeout(r,ms);});};setTimeout(function(){done(UI+'/import?receive=1&error=TIMEOUT');},45000);function withTimeout(p,ms,fallback){return Promise.race([p,new Promise(function(r){setTimeout(function(){r(fallback);},ms);})]);}async function inlineImgRaw(src){if(!src||src.startsWith('data:'))return src;try{var r=await fetch(src,{mode:'cors',credentials:'omit'});if(!r.ok)throw 0;var b=await r.blob();if(!b.type.startsWith('image/'))throw 0;return await new Promise(function(res,rej){var fr=new FileReader();fr.onload=function(){res(fr.result);};fr.onerror=rej;fr.readAsDataURL(b);});}catch(e){return src;}}function inlineImg(src){return withTimeout(inlineImgRaw(src),2000,src);}function extractText(node,role){if(role==='user'){var cands=[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(var i=0;i<cands.length;i++){var c=cands[i];if(c){var t=(c.innerText||c.textContent||'').trim();if(t)return t;}}var clone=node.cloneNode(true);clone.querySelectorAll('button, [role="button"], svg, .sr-only').forEach(function(el){el.remove();});return (clone.innerText||clone.textContent||'').trim();}var md=node.querySelector('.markdown, .prose, [data-message-content]');if(md){var t2=(md.innerText||md.textContent||'').trim();if(t2)return t2;}return (node.innerText||node.textContent||'').trim();}(async function(){try{var nodes=[].slice.call(document.querySelectorAll('[data-message-author-role]'));if(!nodes.length){window.scrollTo(0,document.body.scrollHeight);await delay(500);nodes=[].slice.call(document.querySelectorAll('[data-message-author-role]'));}if(!nodes.length){done(UI+'/import?receive=1&error=NO_MESSAGES');return;}var messages=[];var pending=[];for(var ni=0;ni<nodes.length;ni++){var node=nodes[ni];var role=node.getAttribute('data-message-author-role')||'assistant';var text=extractText(node,role);var rawImgs=[].slice.call(node.querySelectorAll('img')).map(function(img){return img.currentSrc||img.src;}).filter(function(s){return s&&!s.startsWith('data:image/svg');});if(text||rawImgs.length){var m={role:role,text:text,_rawImgs:rawImgs};messages.push(m);pending.push(Promise.all(rawImgs.map(inlineImg)).then(function(target){return function(imgs){target.images=imgs;delete target._rawImgs;};}(m)));}}if(!messages.length){done(UI+'/import?receive=1&error=NO_MESSAGES');return;}await withTimeout(Promise.all(pending),15000,null);for(var mi=0;mi<messages.length;mi++){if(!messages[mi].images){messages[mi].images=messages[mi]._rawImgs||[];delete messages[mi]._rawImgs;}}var title=(document.title||'ChatGPT chat').replace(/^ChatGPT\s*[-—]?\s*/,'')||'ChatGPT chat';var payload={title:title,source_url:location.href,messages:messages};var res=await fetch(API+'/api/public/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});var out=await res.json().catch(function(){return{};});if(!res.ok||!out.id)throw new Error(out.error||('Import failed: '+res.status));done(UI+'/compose?import='+encodeURIComponent(out.id));}catch(err){done(UI+'/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.