// ==UserScript==
// @name Google AI Studio Chat Exporter
// @namespace https://github.com/marmoris-x/tampermonkey-scripts
// @version 5.7.3
// @author marmoris-x
// @description Export AI Studio chat as Markdown via Tampermonkey menu command; non-blocking microphone dialog
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=https://aistudio.google.com/
// @supportURL https://github.com/marmoris-x/tampermonkey-scripts/issues
// @downloadURL https://raw.githubusercontent.com/marmoris-x/tampermonkey-scripts/main/dist/Google%20AI%20Studio%20Chat%20Exporter.user.js
// @updateURL https://raw.githubusercontent.com/marmoris-x/tampermonkey-scripts/main/dist/Google%20AI%20Studio%20Chat%20Exporter.user.js
// @match https://aistudio.google.com/*
// @sandbox raw
// @tag ai
// @grant GM.getValue
// @grant GM.notification
// @grant GM.registerMenuCommand
// @grant GM.setClipboard
// @grant GM.setValue
// @grant GM_addElement
// @grant GM_download
// @run-at document-idle
// @noframes
// ==/UserScript==
(function () {
'use strict';
(function() {
function createLogger(prefix, debugMode) {
debugMode = debugMode || false;
const tag = "[" + prefix + "]";
return {
log: function() {
const args = [tag];
for (let i = 0; i < arguments.length; i++) args.push(arguments[i]);
console.log.apply(console, args);
},
warn: function() {
const args = [tag];
for (let i = 0; i < arguments.length; i++) args.push(arguments[i]);
console.warn.apply(console, args);
},
error: function() {
const args = [tag];
for (let i = 0; i < arguments.length; i++) args.push(arguments[i]);
console.error.apply(console, args);
},
info: function() {
const args = [tag];
for (let i = 0; i < arguments.length; i++) args.push(arguments[i]);
console.info.apply(console, args);
},
debug: function() {
if (debugMode) {
const args = [tag];
for (let i = 0; i < arguments.length; i++) args.push(arguments[i]);
console.debug.apply(console, args);
}
}
};
}
const { log } = createLogger("AI Studio Exporter");
GM_addElement("style", { textContent: [
"/* Native mic dialog: non-blocking, repositioned to bottom-left */",
".cdk-overlay-container:has(ms-mic-audio-dialog) .cdk-overlay-backdrop {",
" pointer-events: none !important;",
" backdrop-filter: none !important;",
" -webkit-backdrop-filter: none !important;",
" background: transparent !important;",
"}",
".cdk-overlay-container:has(ms-mic-audio-dialog) .cdk-global-overlay-wrapper {",
" justify-content: flex-start !important;",
" align-items: flex-end !important;",
" padding: 0 0 80px 16px !important;",
" will-change: transform;",
"}",
".cdk-overlay-container:has(ms-mic-audio-dialog) .cdk-overlay-pane {",
" pointer-events: auto !important;",
" width: 280px !important;",
" height: auto !important;",
" min-width: 0 !important;",
" contain: layout style;",
" animation: ais-mic-fade-in 0.15s ease;",
"}",
"@keyframes ais-mic-fade-in {",
" from { opacity: 0; transform: translateY(8px); }",
" to { opacity: 1; transform: translateY(0); }",
"}",
".cdk-overlay-container:has(ms-mic-audio-dialog) .mat-mdc-dialog-container {",
" --mdc-dialog-container-shape: 12px;",
"}",
".cdk-overlay-container:has(ms-mic-audio-dialog) .mat-mdc-dialog-title {",
" padding: 12px 16px 8px !important;",
" font-size: 14px !important;",
"}",
".cdk-overlay-container:has(ms-mic-audio-dialog) .mat-mdc-dialog-content {",
" padding: 0 16px !important;",
"}",
".cdk-overlay-container:has(ms-mic-audio-dialog) ms-mic-audio-canvas {",
" display: flex;",
" justify-content: center;",
" padding: 8px 0;",
"}",
".cdk-overlay-container:has(ms-mic-audio-dialog) .recording-outer-ring {",
" width: 60px !important;",
" height: 60px !important;",
"}",
".cdk-overlay-container:has(ms-mic-audio-dialog) .recording-indicator {",
" width: 36px !important;",
" height: 36px !important;",
"}",
".cdk-overlay-container:has(ms-mic-audio-dialog) .recording-pulse {",
" width: 60px !important;",
" height: 60px !important;",
"}",
".cdk-overlay-container:has(ms-mic-audio-dialog) .mat-mdc-dialog-actions {",
" padding: 8px 16px 12px !important;",
" min-height: 0 !important;",
"}",
"",
"/* Remove backdrop blur from CDK dialogs */",
".dialog-backdrop-blur-overlay.cdk-overlay-backdrop-showing {",
" backdrop-filter: none !important;",
" -webkit-backdrop-filter: none !important;",
" background: rgba(0, 0, 0, 0.20) !important;",
"}"
].join("\n") });
let includeThoughts = true;
const HEADING_TAGS = { "H1": "#", "H2": "##", "H3": "###", "H4": "####", "H5": "#####", "H6": "######" };
function htmlToMarkdown(el) {
if (!el) return "";
let out = "";
walk(el);
return out.trim().replace(/\n{3,}/g, "\n\n");
function walk(node, depth) {
depth = depth || 1;
if (!node) return;
const children = node.childNodes;
if (!children || children.length === 0) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.replace(/\s+/g, " ");
if (text && text !== " ") out += text;
}
return;
}
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.nodeType === Node.TEXT_NODE) {
const t = child.textContent.replace(/\s+/g, " ");
if (t && t !== " ") out += t;
continue;
}
if (child.nodeType !== Node.ELEMENT_NODE) continue;
const tag = child.tagName.toUpperCase();
if (tag === "BR") {
out += "\n";
continue;
}
if (tag === "HR") {
out += "\n\n---\n\n";
continue;
}
if (HEADING_TAGS[tag]) {
out += "\n\n" + HEADING_TAGS[tag] + " ";
walk(child, depth);
out += "\n\n";
continue;
}
if (tag === "P" || tag === "DIV") {
out += "\n\n";
walk(child, depth);
out += "\n\n";
continue;
}
if (tag === "PRE") {
out += "\n\n```\n" + (child.textContent || "") + "\n```\n\n";
continue;
}
if (tag === "BLOCKQUOTE") {
out += "\n\n> ";
walkInline(child);
out += "\n\n";
continue;
}
if (tag === "UL" || tag === "OL") {
out += "\n\n";
walkList(child, tag === "OL", depth);
out += "\n\n";
continue;
}
if (tag === "TABLE") {
out += "\n\n";
walkTable(child);
out += "\n\n";
continue;
}
if (tag === "IMG") {
const src = child.getAttribute("src") || "";
const alt = child.getAttribute("alt") || "";
out += "";
continue;
}
if (tag === "STRONG" || tag === "B") {
out += "**";
walk(child, depth);
out += "**";
continue;
}
if (tag === "EM" || tag === "I") {
out += "*";
walk(child, depth);
out += "*";
continue;
}
if (tag === "CODE") {
out += "`" + (child.textContent || "") + "`";
continue;
}
if (tag === "A") {
const href = child.getAttribute("href") || "";
out += "[";
walk(child, depth);
out += "](" + href + ")";
continue;
}
if (tag === "DEL" || tag === "S") {
out += "~~";
walk(child, depth);
out += "~~";
continue;
}
if (tag === "U") {
out += "";
walk(child, depth);
out += "";
continue;
}
if (tag === "SPAN") {
const style = child.getAttribute("style") || "";
if (style.includes("font-style: italic") || style.includes("font-style:italic")) {
if (out.length > 0 && out[out.length - 1] !== " " && out[out.length - 1] !== "\n") {
out += " ";
}
out += "*";
walk(child, depth);
out += "*";
} else {
walk(child, depth);
}
continue;
}
walk(child, depth);
}
}
function walkInline(node) {
if (!node) return;
const children = node.childNodes;
if (!children) return;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.nodeType === Node.TEXT_NODE) {
out += child.textContent;
continue;
}
if (child.nodeType !== Node.ELEMENT_NODE) continue;
const tag = child.tagName.toUpperCase();
if (tag === "STRONG" || tag === "B") {
out += "**";
walkInline(child);
out += "**";
} else if (tag === "EM" || tag === "I") {
out += "*";
walkInline(child);
out += "*";
} else if (tag === "CODE") {
out += "`" + (child.textContent || "") + "`";
} else if (tag === "A") {
const href = child.getAttribute("href") || "";
out += "[";
walkInline(child);
out += "](" + href + ")";
} else if (tag === "BR") {
out += " ";
} else if (tag === "IMG") {
const src = child.getAttribute("src") || "";
const alt = child.getAttribute("alt") || "";
out += "";
} else {
walkInline(child);
}
}
}
function walkList(node, ordered, depth) {
const items = [];
(function collectLis(parent) {
const ch = parent.childNodes;
for (let c = 0; c < ch.length; c++) {
if (ch[c].nodeType !== Node.ELEMENT_NODE) continue;
const t = ch[c].tagName.toUpperCase();
if (t === "LI") {
items.push(ch[c]);
} else if (t !== "UL" && t !== "OL") {
collectLis(ch[c]);
}
}
})(node);
for (let i = 0; i < items.length; i++) {
const prefix = " ".repeat(depth - 1) + (ordered ? i + 1 + ". " : "- ");
const before = out.length;
walk(items[i], depth + 1);
const itemContent = out.slice(before).trim().replace(/\n\n+/g, "\n");
out = out.slice(0, before) + prefix + itemContent + "\n";
}
}
function walkTable(node) {
const rows = node.querySelectorAll("tr");
for (let r = 0; r < rows.length; r++) {
const cells = rows[r].querySelectorAll("td, th");
out += "| ";
for (let c = 0; c < cells.length; c++) {
walkInline(cells[c]);
out += " | ";
}
out += "\n";
if (r === 0) {
out += "| ";
for (let c = 0; c < cells.length; c++) {
out += "--- | ";
}
out += "\n";
}
}
}
}
function getThoughts(turnEl) {
const thoughtChunk = turnEl.querySelector("ms-thought-chunk");
if (!thoughtChunk) return "";
const panel = thoughtChunk.querySelector("mat-expansion-panel:not([disabled])");
if (!panel) return "";
const body = panel.querySelector(".mat-expansion-panel-body");
return body ? htmlToMarkdown(body) : "";
}
function getContent(turnEl) {
const primary = turnEl.querySelector("ms-cmark-node");
if (primary) return htmlToMarkdown(primary).trim();
const raw = turnEl.querySelector(".very-large-text-container");
if (raw) return raw.textContent.trim();
const fallback = turnEl.querySelector("ms-prompt-chunk, ms-text-chunk");
if (fallback) return htmlToMarkdown(fallback).trim();
return "";
}
function extractTurn(el) {
const container = el.querySelector(".virtual-scroll-container") || el;
let role = container.getAttribute("data-turn-role") || "";
if (!role) {
const mc = el.querySelector(".model-prompt-container");
role = mc ? "Model" : "Unknown";
}
const tsEl = el.querySelector(".author-label .timestamp");
const timestamp = tsEl ? tsEl.textContent.trim() : "";
const thoughts = getThoughts(el);
const content = getContent(el);
if (!thoughts && !content) return null;
return { role, timestamp, thoughts, content };
}
async function extractAllTurns() {
const scrollButtons = document.querySelectorAll(".items-scrollbar-item button");
if (!scrollButtons.length) {
log("No scrollbar items — DOM-only extraction");
const result2 = [];
const turnEls = document.querySelectorAll("ms-chat-turn");
for (let i = 0; i < turnEls.length; i++) {
const data = extractTurn(turnEls[i]);
if (data) result2.push(data);
}
return result2;
}
log("Found " + scrollButtons.length + " scrollbar items");
const extracted = new Map();
function snapshotVisible() {
const turnEls = document.querySelectorAll("ms-chat-turn");
for (let i = 0; i < turnEls.length; i++) {
const el = turnEls[i];
const id = el.id || "__noid_" + i;
if (extracted.has(id)) continue;
const data = extractTurn(el);
if (data) extracted.set(id, data);
}
}
async function waitForRender() {
await new Promise(function(r) {
setTimeout(r, 500);
});
const deadline = Date.now() + 3e3;
while (Date.now() < deadline) {
const nodes = document.querySelectorAll("ms-chat-turn ms-cmark-node");
let ready = nodes.length > 0;
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].childNodes.length === 0) {
ready = false;
break;
}
}
if (ready) break;
await new Promise(function(r) {
setTimeout(r, 100);
});
}
await new Promise(function(r) {
setTimeout(r, 400);
});
}
for (let i = 0; i < scrollButtons.length; i++) {
scrollButtons[i].click();
await waitForRender();
snapshotVisible();
log("Step " + (i + 1) + "/" + scrollButtons.length + " — captured " + extracted.size + " turns so far");
}
const result = Array.from(extracted.values());
log("Extraction complete: " + result.length + " turns");
return result;
}
function deduplicateParagraphs(markdown) {
const blocks = markdown.split(/\n\n+/);
const seen = new Set();
const unique = [];
for (let i = 0; i < blocks.length; i++) {
const n = blocks[i].trim();
if (!n || seen.has(n)) continue;
seen.add(n);
unique.push(blocks[i]);
}
return unique.join("\n\n");
}
function turnsToMarkdown(turns) {
const lines = [];
for (let i = 0; i < turns.length; i++) {
const t = turns[i];
const showThoughts = includeThoughts && t.thoughts;
const showContent = t.content;
if (!showThoughts && !showContent) continue;
const label = t.role === "User" ? "**User**" : "**Model**";
const ts = t.timestamp ? " _(" + t.timestamp + ")_" : "";
const parts = [label + ts + ":"];
if (showThoughts) {
const tn = t.thoughts.replace(/\s+/g, " ").trim();
const cn = (t.content || "").replace(/\s+/g, " ").trim();
if (tn !== cn) {
parts.push("\nThinking
\n\n" + t.thoughts + "\n\n ");
}
}
if (showContent) {
parts.push(deduplicateParagraphs(t.content));
}
lines.push(parts.join("\n\n"));
}
return lines.join("\n\n---\n\n");
}
function turnsToPlainText(turns) {
return turnsToMarkdown(turns).replace(/\n(.*?)<\/summary>\n\n([\s\S]*?)\n\n<\/details>/g, "[$1]\n$2").replace(/^#{1,6}\s+/gm, "").replace(/\*\*(.*?)\*\*/gs, "$1").replace(/_(.*?)_/gs, "$1").replace(/```[\w]*\n([\s\S]*?)```/g, "$1").replace(/`(.*?)`/g, "$1").replace(/^- /gm, "• ").replace(/^> /gm, " ").replace(/\[([^\]]+)\]/g, "$1").replace(/^---$/gm, "────────────────────────────────────────").replace(/\n{3,}/g, "\n\n").trim();
}
async function exportChat(format) {
const turns = await extractAllTurns();
if (!turns.length) return null;
const text = format === "text" ? turnsToPlainText(turns) : turnsToMarkdown(turns);
return { text, turnCount: turns.length };
}
async function handleCopy() {
const result = await exportChat("markdown");
if (!result) {
GM.notification({
title: "AI Studio Exporter",
text: "No chat turns found on this page.",
timeout: 3e3
});
return;
}
await GM.setClipboard(result.text, { type: "text/plain" });
const size = result.text.length >= 1e3 ? (result.text.length / 1e3).toFixed(1) + "k" : String(result.text.length);
GM.notification({
title: "AI Studio Exporter",
text: `${result.turnCount} turns copied (${size} chars)`,
timeout: 3e3
});
}
async function handleDownload() {
const result = await exportChat("markdown");
if (!result) {
GM.notification({
title: "AI Studio Exporter",
text: "No chat turns found on this page.",
timeout: 3e3
});
return;
}
const now = new Date();
const dateStr = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0") + "-" + String(now.getDate()).padStart(2, "0");
const filename = "ai-studio-chat-" + dateStr + ".md";
const blob = new Blob([result.text], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
GM_download({
url,
name: filename,
saveAs: true,
onload: function() {
URL.revokeObjectURL(url);
},
onerror: function() {
const fallbackUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = fallbackUrl;
a.download = filename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
setTimeout(function() {
a.remove();
URL.revokeObjectURL(fallbackUrl);
}, 2e3);
}
});
GM.notification({
title: "AI Studio Exporter",
text: `${result.turnCount} turns saved as ${filename}`,
timeout: 3e3
});
}
async function handleToggleThoughts() {
includeThoughts = !includeThoughts;
await GM.setValue("includeThoughts", includeThoughts);
GM.notification({
title: "AI Studio Exporter",
text: `Include Thoughts: ${includeThoughts ? "ON" : "OFF"}`,
timeout: 2e3
});
}
async function init() {
includeThoughts = await GM.getValue("includeThoughts", true);
GM.registerMenuCommand("Copy Chat as Markdown", handleCopy);
GM.registerMenuCommand("Download Chat as Markdown", handleDownload);
GM.registerMenuCommand("Toggle Include Thoughts", handleToggleThoughts);
log("Ready — menu commands registered");
}
init();
})();
})();