// ==UserScript== // @name ChatGPT Message Graph (SVG Icons v4.6.2) // @namespace http://tampermonkey.net/ // @version 4.6.2 // @license MIT // @description v4.6.2: Replaced text-based toggle icons with consistent SVG icons for a cleaner, professional UI. Retains all previous features (Horizontal Scroll, Priority Render). // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @run-at document-idle // @grant none // @downloadURL https://update.greasyfork.icu/scripts/558966/ChatGPT%20Message%20Graph%20%28SVG%20Icons%20v462%29.user.js // @updateURL https://update.greasyfork.icu/scripts/558966/ChatGPT%20Message%20Graph%20%28SVG%20Icons%20v462%29.meta.js // ==/UserScript== (function () { "use strict"; // ================================================================================== // 🛠️ CONFIGURATION // ================================================================================== const CONFIG = { panelWidth: 540, nodeWidth: 460, baseNodeHeight: 42, nodeGap: 14, indentPx: 32, maxVisibleLines: 9, selectionTTLms: 20000, debug: false }; const now = () => Date.now(); let isCollapsed = localStorage.getItem("GPT_GRAPH_COLLAPSED") === "true"; // --- Graph State --- let graph = { nodes: new Map(), edges: [], order: [], branchRootId: null }; window.__GPT_GRAPH__ = graph; // --- Icons (SVG) --- const ICONS = { // 减号 (Minimize) MINIMIZE: ``, // 加号 (Expand) EXPAND: `` }; // --- React Fiber --- function getReactFiber(dom) { const key = Object.keys(dom).find(key => key.startsWith("__reactFiber$")); return key ? dom[key] : null; } function getQuoteIdFromReact(userEl) { const quoteBtn = userEl.querySelector('button .line-clamp-3')?.closest('button'); if (!quoteBtn) return null; let curr = getReactFiber(quoteBtn); if (!curr) return null; for(let i=0; i<25; i++) { const props = curr.memoizedProps; if (props?.message?.metadata?.targeted_reply_source_message_id) { return props.message.metadata.targeted_reply_source_message_id; } curr = curr.return; if (!curr) break; } return null; } // --- Outline Extraction --- function extractOutline(role, el) { if (!el) return [{type:'SUM', text:"(error)"}]; const markdown = el.querySelector("[class*='markdown']") || el.querySelector(".prose"); if (role === 'user') { const bubble = el.querySelector('.whitespace-pre-wrap'); const rawText = bubble ? bubble.textContent : el.textContent; const lines = (rawText || "").trim().split('\n').filter(l=>l.trim()); return lines.length > 0 ? [{type:'SUM', text:lines[0].slice(0, 65)}] : [{type:'SUM', text:"(empty)"}]; } if (markdown) { const items = []; const firstChild = markdown.firstElementChild; const isFirstElemHeader = firstChild && /^H[1-6]$/.test(firstChild.tagName); if (firstChild && !isFirstElemHeader) { let summary = firstChild.textContent.trim(); summary = summary.replace(/^结论[::]/, "").replace(/^Summary[::]/, ""); if (summary.length > 2) items.push({type:'SUM', text:summary.slice(0, 60)}); } const allHeaders = Array.from(markdown.querySelectorAll("h1, h2, h3, h4, h5, h6")); const validHeaders = allHeaders.filter(h => !h.closest('pre')); if (validHeaders.length > 0) { const levels = validHeaders.map(h => parseInt(h.tagName.substring(1))); const minLevel = Math.min(...levels); validHeaders.forEach(h => { const txt = h.textContent.trim(); if (!txt) return; const currentLevel = parseInt(h.tagName.substring(1)); if (currentLevel === minLevel) items.push({type:'MAIN', text:txt}); else if (currentLevel === minLevel + 1) items.push({type:'SUB', text:txt}); }); } const mainCount = items.filter(i => i.type === 'MAIN').length; if (mainCount < 2) { const strongs = Array.from(markdown.querySelectorAll("p > strong:first-child, li > strong:first-child")); strongs.forEach(s => { const txt = s.textContent.trim(); if (!txt) return; const looksLikeHeader = /^\d+[\.\)]|^(Step|Phase|Case|Note)\s+\d+|.{2,10}[::]$/.test(txt); const exists = items.some(i => i.text.includes(txt)); if (!exists && looksLikeHeader) items.push({type:'MAIN', text:txt.replace(/[::]$/, "")}); }); } if (items.length === 0) { const fallback = markdown.textContent.trim().slice(0, 60); if(fallback) items.push({type:'SUM', text:fallback}); } return items.length > 0 ? items : [{type:'SUM', text:"..."}]; } return [{type:'SUM', text:el.textContent.slice(0, 60)}]; } // --- Node Management --- function ensureNode(el) { const role = el.getAttribute("data-message-author-role"); const uuid = el.getAttribute("data-message-id"); const id = uuid || el.id || `tmp_${Math.random().toString(36)}`; if (graph.nodes.has(id)) { const node = graph.nodes.get(id); node.el = el; return id; } let rawText = role === 'user' ? (el.querySelector('.whitespace-pre-wrap')?.textContent || el.textContent) : (el.querySelector('.markdown')?.textContent || el.textContent); let outline = []; try { outline = extractOutline(role, el); } catch(e) { outline = [{type:'SUM', text:"(error)"}]; } graph.nodes.set(id, { id, role, el, outline: outline, rawText: rawText || "", depth: 0, height: CONFIG.baseNodeHeight }); graph.order.push(id); return id; } function addEdge(from, to, type) { if (!from || !to || from === to) return; if (!graph.edges.some(e => e.to === to)) graph.edges.push({ from, to, type }); } // --- Rebuild --- function rebuildGraph() { const msgs = document.querySelectorAll('[data-message-author-role]'); if (msgs.length === 0 && graph.nodes.size > 0) { graph.nodes.clear(); graph.edges = []; graph.order = []; render(); return; } const seenIds = new Set(); msgs.forEach(el => { const id = ensureNode(el); seenIds.add(id); }); for (const [id, node] of graph.nodes) { if (!seenIds.has(id)) { graph.nodes.delete(id); graph.edges = graph.edges.filter(e => e.from !== id && e.to !== id); graph.order = graph.order.filter(oid => oid !== id); } } graph.edges = []; for (let i = 0; i < graph.order.length; i++) { const currId = graph.order[i]; const curr = graph.nodes.get(currId); if (!curr) continue; if (curr.role === 'user' && curr.el) { const directParentId = getQuoteIdFromReact(curr.el); if (directParentId && graph.nodes.has(directParentId)) { addEdge(directParentId, currId, "SELECTION_FOLLOW_UP"); } else if (i > 0) { const prevId = graph.order[i-1]; if (graph.nodes.has(prevId)) addEdge(prevId, currId, "CONTINUE"); } } if (curr.role === 'assistant' && i > 0) { const prevId = graph.order[i-1]; const prev = graph.nodes.get(prevId); if (prev && prev.role === 'user') { addEdge(prevId, currId, "ASK_PAIR"); } } } calcDepth(); render(); } function calcDepth() { const parents = new Map(); graph.edges.forEach(e => { if (e.type === "SELECTION_FOLLOW_UP" || e.type === "ASK_PAIR" || e.type === "CONTINUE") { parents.set(e.to, {id: e.from, type: e.type}); } }); const memo = new Map(); function getD(id) { if (memo.has(id)) return memo.get(id); if (!parents.has(id)) return 0; const p = parents.get(id); const cn = graph.nodes.get(id); let d = getD(p.id); if (p.type === "SELECTION_FOLLOW_UP" && cn && cn.role === 'user') d += 1; memo.set(id, d); return d; } graph.nodes.forEach(n => n.depth = getD(n.id)); } // --- UI Render --- const ui = document.createElement('div'); ui.style.cssText = `position:fixed;top:20px;right:20px;width:${CONFIG.panelWidth}px;bottom:20px;z-index:9999;pointer-events:none;display:flex;flex-direction:column;font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;transition:height 0.3s ease, bottom 0.3s ease;`; const canvas = document.createElement('div'); canvas.style.cssText = `pointer-events:auto;flex:1;background:#fff;border:1px solid #e5e7eb;border-radius:12px;display:flex;flex-direction:column;box-shadow:0 10px 30px rgba(0,0,0,0.1);overflow:hidden;transition:all 0.3s ease;`; const header = document.createElement('div'); header.style.cssText = `padding:10px 16px;background:#fff;border-bottom:1px solid #f3f4f6;display:flex;justify-content:space-between;align-items:center;user-select:none;cursor:pointer;`; header.ondblclick = () => toggleCollapse(); // [NEW] Use a span to hold the SVG icon header.innerHTML = `