// ============================================================================= // Chat Bird's-Eye — Universal Chat Extractor (bookmarklet source) // // Click this bookmarklet on any AI chat page. It detects the site, scrapes the // currently-visible conversation(s), accumulates them in localStorage, and // offers a one-click export to birdseye_chats.json — ready to load into the // Chat Bird's-Eye workspace. // // Supported sites (auto-detected by hostname): // chatgpt.com / chat.openai.com → ChatGPT // claude.ai → Claude // gemini.google.com → Gemini // perplexity.ai → Perplexity // x.com (grok) → Grok // copilot.microsoft.com → Copilot // * → Generic fallback (best-effort DOM scrape) // // This file is the readable source. The actual bookmarklet is the minified // single-line version produced by build-bookmarklet.js and embedded in // index.html's "Get real chats" modal. // ============================================================================= (function () { "use strict"; if (window.__BIRDS_EYE_OPEN) { return; } // prevent double-injection window.__BIRDS_EYE_OPEN = true; var STORE_KEY = "birdseye_chats_v1"; var host = location.hostname.replace(/^www\./, ""); var site = /chatgpt\.com|chat\.openai\.com/.test(host) ? "ChatGPT" : /claude\.ai/.test(host) ? "Claude" : /gemini\.google\.com/.test(host) ? "Gemini" : /perplexity\.ai/.test(host) ? "Perplexity" : /x\.com/.test(host) && /grok/i.test(location.pathname) ? "Grok" : /copilot\.microsoft\.com/.test(host) ? "Copilot" : "Unknown"; // ---- helpers ---- function txt(el) { return (el.innerText || el.textContent || "").trim(); } function nowISO() { return new Date().toISOString(); } function uid() { return "ext_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); } function loadStore() { try { return JSON.parse(localStorage.getItem(STORE_KEY) || "[]"); } catch (e) { return []; } } function saveStore(arr) { try { localStorage.setItem(STORE_KEY, JSON.stringify(arr)); } catch (e) {} } // ---- per-site scrapers. Each returns { title, messages: [{role, content, ts}] } or null ---- function scrapeChatGPT() { // visible conversation turns var turns = document.querySelectorAll('[data-testid^="conversation-turn-"]'); if (!turns.length) { // fallback: older DOM turns = document.querySelectorAll('article[data-testid]'); } var msgs = []; for (var i = 0; i < turns.length; i++) { var t = turns[i]; var role = /-2\b|-4\b|user/i.test(t.getAttribute("data-testid") || "") ? "user" : "assistant"; // try to find the actual text container var content = txt(t); if (content) msgs.push({ role: role, content: content, ts: nowISO() }); } if (!msgs.length) return null; var title = document.title.replace(/^ChatGPT\s*[:\-]?\s*/i, "").trim() || "Untitled ChatGPT"; return { title: title, messages: msgs }; } function scrapeClaude() { var userMsgs = document.querySelectorAll('[data-testid="user-message"]'); var allMsgs = []; // Claude interleaves user + assistant in the main thread var thread = document.querySelector('[class*="conversation"]') || document.querySelector("main"); if (!thread) return null; var blocks = thread.querySelectorAll('[data-testid="user-message"], [class*="assistant"]'); if (blocks.length) { for (var i = 0; i < blocks.length; i++) { var b = blocks[i]; var role = b.getAttribute("data-testid") === "user-message" ? "user" : "assistant"; var content = txt(b); if (content) allMsgs.push({ role: role, content: content, ts: nowISO() }); } } // fallback: just user messages if (!allMsgs.length && userMsgs.length) { for (var j = 0; j < userMsgs.length; j++) { var c = txt(userMsgs[j]); if (c) allMsgs.push({ role: "user", content: c, ts: nowISO() }); } } if (!allMsgs.length) return null; var title = document.title.replace(/^Claude\s*[:\-]?\s*/i, "").trim() || "Untitled Claude"; return { title: title, messages: allMsgs }; } function scrapeGemini() { // Gemini renders turns in a scrollable conversation; try broad selectors var turns = document.querySelectorAll("message-content, .response-container, .model-response-text, .user-query, .query-text"); var msgs = []; for (var i = 0; i < turns.length; i++) { var el = turns[i]; var cls = (el.className || "") + " " + (el.tagName || ""); var role = /user|query/i.test(cls) ? "user" : "assistant"; var content = txt(el); if (content) msgs.push({ role: role, content: content, ts: nowISO() }); } if (!msgs.length) return null; var title = document.title.replace(/^Gemini\s*[:\-]?\s*/i, "").trim() || "Untitled Gemini"; return { title: title, messages: msgs }; } function scrapePerplexity() { var queries = document.querySelectorAll('[class*="query"], textarea'); var responses = document.querySelectorAll('[class*="prose"], [class*="answer"]'); var msgs = []; // approximate: interleave by DOM order var all = document.querySelectorAll('[class*="query"], [class*="prose"]'); for (var i = 0; i < all.length; i++) { var el = all[i]; var role = /query/i.test(el.className || "") ? "user" : "assistant"; var content = txt(el); if (content) msgs.push({ role: role, content: content, ts: nowISO() }); } if (!msgs.length) return null; var title = document.title.replace(/^Perplexity\s*[:\-]?\s*/i, "").trim() || "Untitled Perplexity"; return { title: title, messages: msgs }; } function scrapeGeneric() { // best-effort: find the largest text container and split by headings/paragraphs var main = document.querySelector("main") || document.body; var paras = main.querySelectorAll("p, pre, h1, h2, h3"); var msgs = []; var buf = ""; for (var i = 0; i < paras.length; i++) { var t = txt(paras[i]); if (t.length > 10) buf += t + "\n\n"; } if (buf.length > 50) msgs.push({ role: "user", content: buf.trim(), ts: nowISO() }); if (!msgs.length) return null; return { title: document.title.trim() || "Untitled", messages: msgs }; } var scrapers = { "ChatGPT": scrapeChatGPT, "Claude": scrapeClaude, "Gemini": scrapeGemini, "Perplexity": scrapePerplexity, "Grok": scrapeGeneric, "Copilot": scrapeGeneric, "Unknown": scrapeGeneric }; var result = (scrapers[site] || scrapeGeneric)(); if (!result) { toast("Couldn't find a chat on this page (" + site + "). Open a conversation and try again.", "#f87171"); window.__BIRDS_EYE_OPEN = false; return; } // dedupe by title+first-message hash var store = loadStore(); var dedupeKey = result.title + "|" + (result.messages[0] || {}).content.slice(0, 80); var exists = store.some(function (c) { return c._dedupe === dedupeKey; }); if (!exists) { store.push({ id: uid(), source: site, title: result.title, createdAt: nowISO(), updatedAt: nowISO(), messages: result.messages, _dedupe: dedupeKey }); saveStore(store); } var count = store.length; toast( (exists ? "Already had that one. " : "Captured from " + site + ". ") + "Total: " + count + " chat" + (count === 1 ? "" : "s") + " stored. " + "Click here to export →", exists ? "#fbbf24" : "#4ade80", function () { exportJSON(store); } ); // ---- toast UI ---- function toast(msg, color, onClick) { var old = document.getElementById("__birdseye_toast"); if (old) old.remove(); var t = document.createElement("div"); t.id = "__birdseye_toast"; t.style.cssText = [ "position:fixed", "bottom:20px", "right:20px", "z-index:2147483647", "background:#1a1a24", "color:#e8e8f0", "padding:14px 18px", "border-radius:12px", "font:14px/1.5 -apple-system,sans-serif", "max-width:380px", "box-shadow:0 8px 30px rgba(0,0,0,.5)", "border:1px solid " + (color || "#7c83ff"), "cursor:pointer", "transition:opacity .3s" ].join(";") + ";"; t.textContent = msg; t.onclick = function () { if (onClick) onClick(); }; document.body.appendChild(t); setTimeout(function () { t.style.opacity = "0"; setTimeout(function () { t.remove(); }, 400); }, 8000); } function exportJSON(store) { // strip internal _dedupe keys var clean = store.map(function (c) { return { id: c.id, source: c.source, title: c.title, createdAt: c.createdAt, updatedAt: c.updatedAt, messages: c.messages }; }); var blob = new Blob([JSON.stringify(clean, null, 2)], { type: "application/json" }); var url = URL.createObjectURL(blob); var a = document.createElement("a"); a.href = url; a.download = "birdseye_chats.json"; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); // clear store after export so user can start fresh if (confirm("Exported " + clean.length + " chats. Clear the bookmarklet's stored chats?")) { localStorage.removeItem(STORE_KEY); } } window.__BIRDS_EYE_OPEN = false; })();