Skip to content

Instantly share code, notes, and snippets.

@manuc66
Last active March 30, 2026 18:56
Show Gist options
  • Select an option

  • Save manuc66/9da50e3d34b026e0a770f2842a147aef to your computer and use it in GitHub Desktop.

Select an option

Save manuc66/9da50e3d34b026e0a770f2842a147aef to your computer and use it in GitHub Desktop.
Export All DuckDuckGo AI Chats - UserScript
// ==UserScript==
// @name Export All DuckDuckGo AI Chats
// @namespace http://tampermonkey.net/
// @version 2.2
// @description Adds a minimal export button with auto-daily export when new chats are detected
// @author manuc66
// @match *://duckduckgo.com/*
// @match *://duck.ai/chat
// @grant none
// @downloadURL https://update.greasyfork.org/scripts/545609/Export%20All%20DuckDuckGo%20AI%20Chats.user.js
// @updateURL https://update.greasyfork.org/scripts/545609/Export%20All%20DuckDuckGo%20AI%20Chats.meta.js
// ==/UserScript==
(function () {
'use strict';
// Configuration
const STORAGE_KEYS = {
LAST_EXPORT_DATE: 'aiChatExporter_lastExportDate',
LAST_MOST_RECENT_EDIT: 'aiChatExporter_lastMostRecentEdit',
DEVICE_ID_KEY: 'aiChatExporter_deviceID'
};
function getDeviceID() {
let deviceID = localStorage.getItem(STORAGE_KEYS.DEVICE_ID_KEY);
if (!deviceID) {
// Generate a new device ID (UUID format)
deviceID = 'device-' + Date.now() + '-' + Math.random().toString(36).slice(2, 11);
localStorage.setItem(STORAGE_KEYS.DEVICE_ID_KEY, deviceID);
}
return deviceID;
}
// Utility functions
function getFormattedDateTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}_${hours}-${minutes}`;
}
function getTodayDateString() {
const now = new Date();
return now.toDateString(); // Returns format like "Wed Oct 25 2023"
}
function getMostRecentChatEdit(savedAIChats) {
try {
// savedAIChats can be:
// - a JSON string like the old localStorage format ({ chats: [...] })
// - an array of chat objects (IndexedDB saved-chats entries)
// - an object that already has a .chats array
let chatsArray = null;
if (typeof savedAIChats === 'string') {
const data = JSON.parse(savedAIChats);
chatsArray = Array.isArray(data.chats) ? data.chats : null;
} else if (Array.isArray(savedAIChats)) {
chatsArray = savedAIChats;
} else if (savedAIChats && Array.isArray(savedAIChats.chats)) {
chatsArray = savedAIChats.chats;
}
if (!chatsArray || !Array.isArray(chatsArray) || chatsArray.length === 0) {
return null;
}
const mostRecentEdit = chatsArray.reduce((max, chat) => {
// tolerate different timestamp property names
const candidate = chat.lastEdit || chat.updatedAt || chat.modified || chat.last_modified || null;
return candidate && candidate > max ? candidate : max;
},
"1970-01-01T00:00:00Z"
);
return new Date(mostRecentEdit);
} catch (error) {
console.error('Error getting most recent chat edit:', error);
return null;
}
}
// Try to read chats from IndexedDB 'saved-chats' object store.
// Returns an array of chat objects, or null if not found.
async function fetchSavedChatsFromIndexedDB() {
if (!window.indexedDB) return null;
// Helper to open DB and check for the store
async function tryOpenDB(name) {
return new Promise((resolve) => {
try {
const req = indexedDB.open(name);
req.onsuccess = (e) => {
const db = e.target.result;
if (db.objectStoreNames && db.objectStoreNames.contains('saved-chats')) {
const tx = db.transaction('saved-chats', 'readonly');
const store = tx.objectStore('saved-chats');
const getAllReq = store.getAll();
getAllReq.onsuccess = (ev) => {
const all = ev.target.result;
db.close();
resolve(all || null);
};
getAllReq.onerror = () => {
db.close();
resolve(null);
};
} else {
db.close();
resolve(null);
}
};
req.onerror = () => resolve(null);
} catch (e) {
console.warn('IndexedDB open error for', name, e);
resolve(null);
}
});
}
// If supported, enumerate known databases first
if (indexedDB.databases) {
try {
const dbs = await indexedDB.databases();
for (const info of dbs) {
if (!info.name) continue;
const res = await tryOpenDB(info.name);
if (res) return res;
}
} catch (e) {
console.warn('indexedDB.databases() failed:', e);
}
}
// Fallback: try some likely DB names
const likelyNames = ['duckai', 'duck.ai', 'duckduckgo-ai', 'duckduckgo', 'savedAIChats'];
for (const name of likelyNames) {
const res = await tryOpenDB(name);
if (res) return res;
}
return null;
}
function getExportFileName() {
const datetime = getFormattedDateTime();
const deviceID = getDeviceID();
return `savedAIChats_${datetime}_${deviceID}.json`;
}
async function exportChats(isAutomatic = false) {
// Prefer localStorage legacy key if present, otherwise try IndexedDB
let data = null;
const savedAIChatsLS = localStorage.getItem('savedAIChats');
if (savedAIChatsLS) {
try {
data = JSON.parse(savedAIChatsLS);
} catch (e) {
console.warn('Failed parsing savedAIChats from localStorage:', e);
}
}
if (!data) {
// Try IndexedDB
const entries = await fetchSavedChatsFromIndexedDB();
if (!entries) {
if (!isAutomatic) {
alert('No saved chats found in localStorage or IndexedDB (saved-chats).');
}
return false;
}
data = { chats: entries };
}
try {
const filename = getExportFileName();
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
// Programmatically click the link to trigger the download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Update tracking data
const today = getTodayDateString();
const mostRecentEdit = getMostRecentChatEdit(data);
const chatCount = data.chats ? data.chats.length : 0;
localStorage.setItem(STORAGE_KEYS.LAST_EXPORT_DATE, today);
if (mostRecentEdit) {
localStorage.setItem(STORAGE_KEYS.LAST_MOST_RECENT_EDIT, mostRecentEdit.toISOString());
}
if (isAutomatic) {
showNotification(`Auto-exported ${chatCount} chats (${filename})`);
}
return true;
} catch (error) {
console.error('Export failed:', error);
if (!isAutomatic) {
alert('Export failed: ' + error.message);
}
return false;
}
}
function showNotification(message) {
const notification = document.createElement('div');
notification.textContent = message;
notification.style.position = 'fixed';
notification.style.top = '20px';
notification.style.right = '20px';
notification.style.backgroundColor = '#28a745';
notification.style.color = 'white';
notification.style.padding = '10px 15px';
notification.style.borderRadius = '5px';
notification.style.zIndex = '10000';
notification.style.fontSize = '14px';
notification.style.maxWidth = '300px';
notification.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
notification.style.opacity = '0';
notification.style.transition = 'opacity 0.3s ease';
document.body.appendChild(notification);
// Fade in
setTimeout(() => notification.style.opacity = '1', 100);
// Fade out and remove after 4 seconds
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => document.body.removeChild(notification), 300);
}, 4000);
}
async function checkForNewChats() {
// Try localStorage first, otherwise try IndexedDB
let data = null;
const savedAIChatsLS = localStorage.getItem('savedAIChats');
if (savedAIChatsLS) {
try { data = JSON.parse(savedAIChatsLS); } catch (e) { data = null; }
}
if (!data) {
const entries = await fetchSavedChatsFromIndexedDB();
if (!entries) return false;
data = { chats: entries };
}
try {
const currentMostRecentEdit = getMostRecentChatEdit(data);
if (!currentMostRecentEdit) return false;
const lastExportDate = localStorage.getItem(STORAGE_KEYS.LAST_EXPORT_DATE);
const lastMostRecentEditStr = localStorage.getItem(STORAGE_KEYS.LAST_MOST_RECENT_EDIT);
const today = getTodayDateString();
const isNewDay = lastExportDate !== today;
let hasNewChats = false;
if (lastMostRecentEditStr) {
const lastMostRecentEdit = new Date(lastMostRecentEditStr);
hasNewChats = currentMostRecentEdit > lastMostRecentEdit;
} else {
// First time running, consider it as having new chats
hasNewChats = true;
}
return isNewDay && hasNewChats;
} catch (error) {
console.error('Error checking for new chats:', error);
return false;
}
}
function startAutoExportCheck() {
// Check immediately (async)
(async () => {
try {
if (await checkForNewChats()) {
await exportChats(true);
}
} catch (e) { console.error(e); }
})();
// Then check every 30 minutes
setInterval(() => {
(async () => {
try {
if (await checkForNewChats()) {
await exportChats(true);
}
} catch (e) { console.error(e); }
})();
}, 30 * 60 * 1000); // 30 minutes
}
// Create the export button
const exportButton = document.createElement('button');
exportButton.innerText = '💾';
exportButton.title = 'Export AI Chats (Auto-exports daily when new chats detected)';
exportButton.id = 'ai-chat-export-btn';
// Base styles for the button
exportButton.style.position = 'fixed';
exportButton.style.top = '50%';
exportButton.style.right = '10px';
exportButton.style.transform = 'translateY(-50%)';
exportButton.style.width = '40px';
exportButton.style.height = '40px';
exportButton.style.padding = '0';
exportButton.style.backgroundColor = '#007bff';
exportButton.style.color = '#fff';
exportButton.style.border = 'none';
exportButton.style.borderRadius = '50%';
exportButton.style.cursor = 'pointer';
exportButton.style.zIndex = '9999';
exportButton.style.fontSize = '16px';
exportButton.style.display = 'flex';
exportButton.style.alignItems = 'center';
exportButton.style.justifyContent = 'center';
exportButton.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
exportButton.style.transition = 'all 0.2s ease';
// Add hover effects
exportButton.addEventListener('mouseenter', function () {
this.style.backgroundColor = '#0056b3';
this.style.transform = 'translateY(-50%) scale(1.1)';
});
exportButton.addEventListener('mouseleave', function () {
this.style.backgroundColor = '#007bff';
this.style.transform = 'translateY(-50%) scale(1)';
});
// Responsive styles using CSS
const style = document.createElement('style');
style.innerHTML = `
@media (max-width: 600px) {
#ai-chat-export-btn {
width: 35px !important;
height: 35px !important;
font-size: 14px !important;
right: 8px !important;
}
}
@media (max-width: 400px) {
#ai-chat-export-btn {
width: 30px !important;
height: 30px !important;
font-size: 12px !important;
right: 5px !important;
}
}
`;
document.head.appendChild(style);
// Append the button to the body
document.body.appendChild(exportButton);
// Add click event to the button
exportButton.addEventListener('click', function () {
// Add click animation
this.style.transform = 'translateY(-50%) scale(0.95)';
setTimeout(() => {
this.style.transform = 'translateY(-50%) scale(1)';
}, 100);
// call async export and ignore result
(async () => { await exportChats(false); })();
});
// Start the auto-export monitoring (async)
// Wait a bit for the page to fully load
setTimeout(() => { startAutoExportCheck(); }, 3000);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment