Created
April 18, 2026 17:42
-
-
Save lumpsoid/d03f12aea800af5a668944cf734cfa4c to your computer and use it in GitHub Desktop.
openrouter current chat backup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| async function backupCurrentRoom() { | |
| // --- find db --- | |
| const dbs = await indexedDB.databases(); | |
| const dbEntry = dbs.find(db => db.name.includes(':org_')) | |
| ?? dbs.find(db => db.name.startsWith('openrouter:playground:')); | |
| if (!dbEntry) { | |
| console.error('No OpenRouter database found. Make sure you are on openrouter.ai'); | |
| return; | |
| } | |
| // --- get current room id from the URL --- | |
| const roomId = new URLSearchParams(location.search).get('room'); | |
| if (!roomId) { | |
| console.error('No room= param found in URL. Make sure a chat is open.'); | |
| return; | |
| } | |
| console.log(`Backing up room: ${roomId} from ${dbEntry.name}`); | |
| // --- open db --- | |
| const db = await new Promise((res, rej) => { | |
| const r = indexedDB.open(dbEntry.name); | |
| r.onsuccess = e => res(e.target.result); | |
| r.onerror = e => rej(e.target.error); | |
| }); | |
| const storeName = dbEntry.name; | |
| // --- fetch only the keys we need for this room --- | |
| function iget(key) { | |
| return new Promise((res, rej) => { | |
| const tx = db.transaction(storeName, 'readonly'); | |
| const req = tx.objectStore(storeName).get(key); | |
| req.onsuccess = () => res(req.result?.value ?? null); | |
| req.onerror = e => rej(e.target.error); | |
| }); | |
| } | |
| // --- room + manifest --- | |
| const [room, manifest] = await Promise.all([ | |
| iget(`v3:room:${roomId}`), | |
| iget(`v3:manifest:${roomId}`), | |
| ]); | |
| if (!room) { | |
| console.error(`Room ${roomId} not found in IndexedDB.`); | |
| db.close(); | |
| return; | |
| } | |
| if (!manifest) { | |
| console.error(`Manifest for ${roomId} not found.`); | |
| db.close(); | |
| return; | |
| } | |
| // --- fetch all related records in parallel --- | |
| const [chars, msgs, items] = await Promise.all([ | |
| Promise.all((manifest.characterIds ?? []).map(id => iget(`v3:character:${id}`))), | |
| Promise.all((manifest.messageIds ?? []).map(id => iget(`v3:message:${id}`))), | |
| Promise.all((manifest.itemIds ?? []).map(id => iget(`v3:item:${id}`))), | |
| ]); | |
| db.close(); | |
| // --- index items by id for quick lookup --- | |
| const itemMap = Object.fromEntries( | |
| items.filter(Boolean).map(item => [item.id, item]) | |
| ); | |
| // --- helpers --- | |
| function itemToText(item) { | |
| const content = item?.data?.content; | |
| if (!Array.isArray(content)) return null; | |
| return content.map(b => b.text ?? b.content ?? '').filter(Boolean).join('\\n') || null; | |
| } | |
| // --- assemble characters --- | |
| const roomChars = chars.filter(Boolean).map(c => ({ | |
| id: c.id, | |
| model: c.model, | |
| samplingParameters: c.samplingParameters, | |
| reasoning: c.reasoning ?? null, | |
| isRemoved: c.isRemoved ?? false, | |
| isDisabled: c.isDisabled ?? false, | |
| })); | |
| const charModelMap = Object.fromEntries(roomChars.map(c => [c.id, c.model])); | |
| // --- assemble messages --- | |
| const roomMessages = msgs.filter(Boolean).map(msg => { | |
| const isUser = msg.characterId === 'USER'; | |
| const role = isUser ? 'user' : 'assistant'; | |
| const model = isUser ? null : (charModelMap[msg.characterId] ?? msg.characterId); | |
| const resolvedItems = (msg.items ?? []).map(ref => ({ | |
| type: ref.type, | |
| id: ref.id, | |
| text: itemToText(itemMap[ref.id]), | |
| })); | |
| return { | |
| id: msg.id, | |
| role, | |
| model, | |
| createdAt: msg.createdAt, | |
| updatedAt: msg.updatedAt, | |
| parentMessageId: msg.parentMessageId ?? null, | |
| isEdited: msg.isEdited ?? false, | |
| context: msg.context ?? null, | |
| text: resolvedItems.filter(i => i.type === 'message') .map(i => i.text).filter(Boolean).join('\\n') || null, | |
| reasoning: resolvedItems.filter(i => i.type === 'reasoning').map(i => i.text).filter(Boolean).join('\\n') || null, | |
| metadata: msg.metadata ?? null, | |
| }; | |
| }); | |
| // --- download --- | |
| const payload = { | |
| exportedAt: new Date().toISOString(), | |
| dbName: dbEntry.name, | |
| room: { | |
| id: room.id, | |
| title: room.title, | |
| createdAt: room.createdAt, | |
| updatedAt: room.updatedAt, | |
| lastActivityAt: room.lastActivityAt ?? null, | |
| isPinned: room.isPinned ?? false, | |
| characters: roomChars, | |
| messages: roomMessages, | |
| }, | |
| }; | |
| const safe = room.title | |
| // Remove everything except letters, digits, underscore, hyphen, dot or whitespace | |
| .replace(/[^\w\s.-]/g, '') | |
| // Replace *every* dot with a hyphen | |
| .replace(/\./g, '-') | |
| // Collapse whitespace → single hyphen | |
| .trim() | |
| .replace(/\s+/g, '-') | |
| // also collapse any duplicate hyphens that may have been produced | |
| .replace(/-+/g, '-') | |
| // Trim leading/trailing hyphens/underscores and enforce a max length of 60 | |
| .replace(/^[-_]+|[-_]+$/g, '') | |
| .slice(0, 60); | |
| const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = `${roomId}__${safe}.json`; | |
| a.click(); | |
| URL.revokeObjectURL(a.href); | |
| console.log(`✅ Exported "${room.title}" — ${roomMessages.length} messages`); | |
| } | |
| // USAGE — just call this while the chat is open: | |
| // backupCurrentRoom() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment