| 1 | import { newSession, finalizeSession, pushTurn, flattenParts, looksSynthetic, noteAssistantRefusal } from './shared.js'; |
| 2 | |
| 3 | function conversationList(parsed) { |
| 4 | if (Array.isArray(parsed)) return parsed; |
| 5 | if (parsed && Array.isArray(parsed.conversations)) return parsed.conversations; |
| 6 | if (parsed && parsed.mapping && typeof parsed.mapping === 'object') return [parsed]; |
| 7 | return []; |
| 8 | } |
| 9 | |
| 10 | export function detectChatGPT(parsed) { |
| 11 | const list = conversationList(parsed); |
| 12 | if (!list.length) return false; |
| 13 | const first = list[0]; |
| 14 | return Boolean(first && first.mapping && typeof first.mapping === 'object'); |
| 15 | } |
| 16 | |
| 17 | export function parseChatGPT(parsed, path) { |
| 18 | const conversations = conversationList(parsed); |
| 19 | const sessions = []; |
| 20 | for (let i = 0; i < conversations.length; i++) { |
| 21 | const convo = conversations[i]; |
| 22 | if (!convo || !convo.mapping) continue; |
| 23 | const session = sessionFromConversation(convo, path, i); |
| 24 | if (session.prompts.length) sessions.push(session); |
| 25 | } |
| 26 | return sessions; |
| 27 | } |
| 28 | |
| 29 | function sessionFromConversation(convo, path, index) { |
| 30 | const id = convo.conversation_id || convo.id || `chatgpt-${index + 1}`; |
| 31 | const session = newSession(path, id); |
| 32 | if (convo.title) session.title = convo.title; |
| 33 | |
| 34 | const ordered = orderNodes(convo.mapping); |
| 35 | let turn = 0; |
| 36 | for (const node of ordered) { |
| 37 | const msg = node.message; |
| 38 | if (!msg || !msg.author) continue; |
| 39 | const role = msg.author.role; |
| 40 | const text = flattenParts(msg.content && msg.content.parts); |
| 41 | const ts = msg.create_time ? new Date(msg.create_time * 1000).toISOString() : null; |
| 42 | |
| 43 | if (role === 'user') { |
| 44 | if (looksSynthetic(text)) continue; |
| 45 | pushTurn(session, ++turn, text, ts); |
| 46 | } else if (role === 'assistant') { |
| 47 | session.stats.assistantLines++; |
| 48 | if (msg.metadata && msg.metadata.model_slug) session.stats.models.add(msg.metadata.model_slug); |
| 49 | noteAssistantRefusal(session, text); |
| 50 | } else if (role === 'tool') { |
| 51 | session.stats.toolUses++; |
| 52 | } |
| 53 | } |
| 54 | return finalizeSession(session); |
| 55 | } |
| 56 | |
| 57 | function orderNodes(mapping) { |
| 58 | const nodes = Object.values(mapping).filter((n) => n && n.message); |
| 59 | const withTime = nodes.filter((n) => typeof n.message.create_time === 'number'); |
| 60 | if (withTime.length === nodes.length && nodes.length) { |
| 61 | return nodes.slice().sort((a, b) => a.message.create_time - b.message.create_time); |
| 62 | } |
| 63 | return walkFromRoot(mapping); |
| 64 | } |
| 65 | |
| 66 | function walkFromRoot(mapping) { |
| 67 | let rootId = null; |
| 68 | for (const [id, node] of Object.entries(mapping)) { |
| 69 | if (node && (node.parent === null || node.parent === undefined)) { |
| 70 | rootId = id; |
| 71 | break; |
| 72 | } |
| 73 | } |
| 74 | const out = []; |
| 75 | const seen = new Set(); |
| 76 | let cur = rootId; |
| 77 | while (cur && mapping[cur] && !seen.has(cur)) { |
| 78 | seen.add(cur); |
| 79 | out.push(mapping[cur]); |
| 80 | const children = mapping[cur].children || []; |
| 81 | cur = children.length ? children[children.length - 1] : null; |
| 82 | } |
| 83 | return out; |
| 84 | } |