chat-birdseye / extractor.js
josrano's picture
Add universal bookmarklet extractor + ChatGPT/Claude export-file auto-detect
664b8ad verified
Raw
History Blame Contribute Delete
9.35 kB
// =============================================================================
// 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;
})();