Export AI Studio chat history. 1. i18n support (CN/EN/DE/RU/JA). 2. Draggable button. 3. Auto system prompt detection. 4. Perfect Markdown formatting.
// ==UserScript==
// @name AI Studio Chat Exporter (Markdown & Code Block Support)
// @namespace http://tampermonkey.net/
// @version 5.0
// @author Tokisaki Galaxy
// @match https://aistudio.google.com/prompts/*
// @description Export AI Studio chat history. 1. i18n support (CN/EN/DE/RU/JA). 2. Draggable button. 3. Auto system prompt detection. 4. Perfect Markdown formatting.
// @icon https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant none
// @license AGPL-3.0
// ==/UserScript==
(function() {
'use strict';
// --- 🌍 i18n Configuration ---
const MESSAGES = {
'zh': {
exportBtn: "导出 JSON",
wait: "请稍候...",
getSys: "获取系统提示词...",
resetView: "重置视图...",
analyzing: "分析抓取中...",
packaging: "生成 JSON...",
noChat: "未找到聊天区域,请确保处于对话页面。",
sysFound: "已获取系统提示词",
sysHidden: "展开侧边栏获取..."
},
'en': {
exportBtn: "Export JSON",
wait: "Please wait...",
getSys: "Fetching System Prompt...",
resetView: "Resetting View...",
analyzing: "Analyzing...",
packaging: "Generating JSON...",
noChat: "Chat container not found.",
sysFound: "System prompt captured",
sysHidden: "Expanding sidebar..."
},
'de': {
exportBtn: "JSON Exportieren",
wait: "Bitte warten...",
getSys: "System-Prompt abrufen...",
resetView: "Ansicht zurücksetzen...",
analyzing: "Analysieren...",
packaging: "JSON erstellen...",
noChat: "Chat-Bereich nicht gefunden.",
sysFound: "System-Prompt erfasst",
sysHidden: "Seitenleiste erweitern..."
},
'ru': {
exportBtn: "Экспорт JSON",
wait: "Подождите...",
getSys: "Получение System Prompt...",
resetView: "Сброс вида...",
analyzing: "Анализ...",
packaging: "Создание JSON...",
noChat: "Область чата не найдена.",
sysFound: "System prompt получен",
sysHidden: "Расширение боковой панели..."
},
'ja': {
exportBtn: "JSON をエクスポート",
wait: "少々お待ちください...",
getSys: "システムプロンプトを取得中...",
resetView: "ビューをリセット中...",
analyzing: "解析中...",
packaging: "JSON を生成中...",
noChat: "チャット領域が見つかりませんでした。",
sysFound: "システムプロンプトを取得しました",
sysHidden: "サイドバーを展開しています..."
}
};
// Detect Browser Language
const langCode = (navigator.language || navigator.userLanguage).slice(0, 2);
const t = (key) => MESSAGES[langCode]?.[key] || MESSAGES['en'][key];
// --- Config ---
const CONFIG = {
scrollStep: 350,
scrollDelay: 1200,
uiDelay: 1000,
};
let isExporting = false;
// --- UI: Draggable Button ---
function createExportButton() {
if (document.getElementById('ai-studio-export-btn')) return;
const btn = document.createElement('button');
btn.id = 'ai-studio-export-btn';
btn.innerText = t('exportBtn');
Object.assign(btn.style, {
position: 'fixed',
top: '20px',
left: '50%',
transform: 'translateX(-50%)',
zIndex: '99999',
padding: '10px 16px',
backgroundColor: '#1a73e8',
color: 'white',
border: 'none',
borderRadius: '20px',
cursor: 'move',
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
fontWeight: 'bold',
fontSize: '14px',
fontFamily: 'sans-serif',
transition: 'background-color 0.2s, transform 0.1s',
userSelect: 'none'
});
// Draggable Logic
let isDragging = false;
let hasMoved = false;
let startX, startY;
btn.addEventListener('mousedown', function(e) {
isDragging = true;
hasMoved = false;
const rect = btn.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
btn.style.transform = 'none';
btn.style.left = rect.left + 'px';
btn.style.top = rect.top + 'px';
btn.style.opacity = '0.9';
});
document.addEventListener('mousemove', function(e) {
if (!isDragging) return;
hasMoved = true;
e.preventDefault();
btn.style.left = `${e.clientX - startX}px`;
btn.style.top = `${e.clientY - startY}px`;
});
document.addEventListener('mouseup', function() {
if (isDragging) {
isDragging = false;
btn.style.opacity = '1';
}
});
btn.addEventListener('click', function(e) {
if (hasMoved) {
e.preventDefault();
e.stopPropagation();
return;
}
startExportProcess();
});
document.body.appendChild(btn);
}
function updateBtn(text, disabled = false) {
const btn = document.getElementById('ai-studio-export-btn');
if (btn) {
btn.innerText = text;
btn.disabled = disabled;
btn.style.backgroundColor = disabled ? '#7f8c8d' : '#1a73e8';
btn.style.cursor = disabled ? 'wait' : 'move';
}
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// --- Logic 1: System Instruction ---
async function getSystemInstruction() {
updateBtn(t('getSys'));
let target = document.querySelector('ms-system-instructions-panel .subtitle');
if (target && target.innerText.trim()) return target.innerText.trim();
const toggleBtn = document.querySelector('.runsettings-toggle-button');
if (toggleBtn) {
console.log(t('sysHidden'));
toggleBtn.click();
await sleep(CONFIG.uiDelay);
target = document.querySelector('ms-system-instructions-panel .subtitle');
let text = target ? target.innerText.trim() : "";
if(!text) {
const fallback = document.querySelector('ms-system-instruction-editor textarea');
if(fallback) text = fallback.value;
}
toggleBtn.click();
await sleep(500);
return text;
}
return "";
}
// --- Logic 2: Markdown Converter ---
function domToMarkdown(node) {
if (!node) return "";
const skipClasses = ['author-label', 'actions-container', 'turn-footer', 'thinking-progress-icon', 'thought-collapsed-text', 'mat-icon'];
if (node.classList && skipClasses.some(c => node.classList.contains(c))) return "";
if (node.tagName === 'MS-THOUGHT-CHUNK' || node.tagName === 'BUTTON') return "";
// Code Blocks
if (node.tagName === 'MS-CODE-BLOCK') {
let lang = "text";
const titleSpan = node.querySelector('.title span:last-child');
if (titleSpan) lang = titleSpan.innerText.trim();
const codeEl = node.querySelector('code');
const codeText = codeEl ? codeEl.innerText : node.innerText;
return `\n\`\`\`${lang}\n${codeText.trim()}\n\`\`\`\n`;
}
// List
if (node.tagName === 'LI') return `- ${parseChildren(node).trim()}\n`;
// Heading
if (/^H[1-6]$/.test(node.tagName)) {
const level = parseInt(node.tagName[1]);
return `\n${'#'.repeat(level)} ${parseChildren(node).trim()}\n`;
}
// Paragraph & LineBreak
if (node.tagName === 'P') return `\n${parseChildren(node).trim()}\n\n`;
if (node.tagName === 'BR') return "\n";
if (node.nodeType === Node.TEXT_NODE) return node.textContent;
let result = parseChildren(node);
// Formatting
if (['STRONG', 'B'].includes(node.tagName)) result = `**${result}**`;
if (['EM', 'I'].includes(node.tagName)) result = `*${result}*`;
if (node.classList && node.classList.contains('inline-code')) result = `\`${result}\``;
return result;
}
function parseChildren(node) {
let text = "";
node.childNodes.forEach(child => {
text += domToMarkdown(child);
});
return text;
}
function extractCleanMarkdown(turnElement) {
const contentDiv = turnElement.querySelector('.turn-content');
if (!contentDiv) return null;
let md = domToMarkdown(contentDiv);
md = md.replace(/\n{3,}/g, '\n\n').trim();
if (!md || md === "Model" || md === "User") return null;
return md;
}
// --- Logic 3: Main Flow ---
async function startExportProcess() {
if (isExporting) return;
isExporting = true;
const container = document.querySelector('ms-autoscroll-container');
if (!container) {
alert(t('noChat'));
isExporting = false;
return;
}
// 1. System Prompt
const sysInstruction = await getSystemInstruction();
// 2. Scroll & Scrape
const messageMap = new Map();
const idOrder = [];
updateBtn(t('resetView'));
container.scrollTo({ top: 0, behavior: 'instant' });
await sleep(1500);
let lastScrollTop = -1;
let stuckCounter = 0;
while (true) {
const visibleTurns = document.querySelectorAll('ms-chat-turn');
visibleTurns.forEach(turn => {
const uid = turn.id;
if (!uid) return;
if (!idOrder.includes(uid)) idOrder.push(uid);
let role = 'user';
if (turn.querySelector('.model-prompt-container') || turn.getAttribute('data-turn-role') === 'Model') {
role = 'assistant';
}
const content = extractCleanMarkdown(turn);
if (content) {
messageMap.set(uid, { role, content });
}
});
// Stop condition
const isBottom = Math.abs(container.scrollHeight - container.clientHeight - container.scrollTop) < 20;
if (Math.abs(container.scrollTop - lastScrollTop) < 2) stuckCounter++;
else stuckCounter = 0;
const percent = Math.min(99, Math.floor((container.scrollTop / (container.scrollHeight - container.clientHeight)) * 100));
updateBtn(`${t('analyzing')} ${percent}%`);
if (isBottom || stuckCounter >= 3) break;
lastScrollTop = container.scrollTop;
container.scrollBy({ top: CONFIG.scrollStep, behavior: 'smooth' });
await sleep(CONFIG.scrollDelay);
}
// 3. Export
updateBtn(t('packaging'));
const validMessages = [];
idOrder.forEach(id => {
if (messageMap.has(id)) {
validMessages.push(messageMap.get(id));
}
});
downloadFile({
system_instruction: sysInstruction,
messages: validMessages
});
updateBtn(t('exportBtn'), false);
isExporting = false;
}
function downloadFile(data) {
let title = "aistudio_chat";
try {
const h1 = document.querySelector('.page-title h1');
if (h1) title = h1.innerText.trim().replace(/[\\/:*?"<>|]/g, '_');
} catch(e) {}
const blob = new Blob([JSON.stringify(data, null, 2)], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${title}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function init() {
const observer = new MutationObserver(() => createExportButton());
observer.observe(document.body, { childList: true, subtree: true });
createExportButton();
}
window.addEventListener('load', init);
if (document.readyState === 'complete') init();
})();