|
// ==UserScript== |
|
// @name DeepSeek ➜ Gist |
|
// @namespace https://github.com/strickvl/deepseek-chat-share |
|
// @version 0.1 |
|
// @description Add a “Share to GitHub Gist” button to chat.deepseek.com that captures the full conversation in Markdown. |
|
// @author Alex Strick van Linschoten |
|
// @match https://chat.deepseek.com/* |
|
// @icon https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png |
|
// @grant GM_getValue |
|
// @grant GM_setValue |
|
// @grant GM_xmlhttpRequest |
|
// @grant GM_addStyle |
|
// @connect api.github.com |
|
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/turndown.min.js |
|
// ==/UserScript== |
|
|
|
/** |
|
* DeepSeek Chat → GitHub Gist |
|
* ------------------------------------------------------------- |
|
* v0.1 |
|
*/ |
|
(function () { |
|
"use strict"; |
|
|
|
/*───────────────────────────────────────────────*/ |
|
/* CONFIG & UTILITIES */ |
|
/*───────────────────────────────────────────────*/ |
|
async function getGitHubPAT() { |
|
let pat = GM_getValue("github_pat", ""); |
|
if (!pat) { |
|
pat = prompt( |
|
"GitHub Personal‑Access Token (scope: gist) – stored **locally**:", |
|
"" |
|
); |
|
if (!pat) throw new Error("PAT not provided."); |
|
GM_setValue("github_pat", pat.trim()); |
|
} |
|
return pat.trim(); |
|
} |
|
|
|
const turndown = new TurndownService({ |
|
headingStyle: "atx", |
|
codeBlockStyle: "fenced", |
|
fence: "```", |
|
}); |
|
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1); |
|
|
|
/*───────────────────────────────────────────────*/ |
|
/* SCRAPE CHAT → MD */ |
|
/*───────────────────────────────────────────────*/ |
|
function scrapeConversation() { |
|
const blocks = document.querySelectorAll("div.dad65929, div._4f9bf79"); |
|
const msgs = []; |
|
|
|
blocks.forEach((b) => { |
|
const isAssistant = !!b.querySelector(".ds-markdown"); |
|
let content = ""; |
|
if (isAssistant) { |
|
const md = b.querySelector(".ds-markdown"); |
|
content = turndown.turndown(md.innerHTML).trim(); |
|
} else { |
|
const user = b.querySelector(".fbb737a4") || b; |
|
content = user.textContent.trim(); |
|
} |
|
if (content) msgs.push({ role: isAssistant ? "assistant" : "user", content }); |
|
}); |
|
|
|
return msgs; |
|
} |
|
|
|
function toMarkdown(convo) { |
|
const out = ["# DeepSeek Conversation", ""]; |
|
convo.forEach((t) => { |
|
const emoji = t.role === "user" ? "🧑" : "🤖"; |
|
out.push(`## ${emoji} ${cap(t.role)}`, "", t.content, "", "---", ""); |
|
}); |
|
return out.join("\n").replace(/\n{3,}/g, "\n\n").trim(); |
|
} |
|
|
|
/*───────────────────────────────────────────────*/ |
|
/* GITHUB GIST API */ |
|
/*───────────────────────────────────────────────*/ |
|
function uploadGist(markdown, pat) { |
|
return new Promise((res, rej) => { |
|
GM_xmlhttpRequest({ |
|
method: "POST", |
|
url: "https://api.github.com/gists", |
|
headers: { |
|
Authorization: `token ${pat}`, |
|
Accept: "application/vnd.github+json", |
|
}, |
|
data: JSON.stringify({ |
|
description: "DeepSeek Chat Conversation", |
|
public: false, |
|
files: { |
|
deepseek_conversation: { |
|
filename: "deepseek_conversation.md", |
|
content: markdown, |
|
}, |
|
}, |
|
}), |
|
onload: (r) => { |
|
if (r.status >= 200 && r.status < 300) |
|
res(JSON.parse(r.responseText).html_url); |
|
else rej(new Error(`GitHub API error (${r.status})`)); |
|
}, |
|
onerror: () => rej(new Error("Network error contacting GitHub")), |
|
}); |
|
}); |
|
} |
|
|
|
/*───────────────────────────────────────────────*/ |
|
/* BANNER */ |
|
/*───────────────────────────────────────────────*/ |
|
function banner(msg, kind = "info", dur = 4000) { |
|
const c = { info: "#2563eb", success: "#15803d", error: "#dc2626" }[kind]; |
|
const el = Object.assign(document.createElement("div"), { |
|
textContent: msg, |
|
}); |
|
el.style.cssText = `position:fixed;top:0;left:0;right:0;padding:8px 12px;font:14px system-ui,sans-serif;color:#fff;background:${c};z-index:99999;text-align:center`; |
|
document.body.appendChild(el); |
|
if (dur) setTimeout(() => el.remove(), dur); |
|
} |
|
|
|
/*───────────────────────────────────────────────*/ |
|
/* BUTTON CREATION */ |
|
/*───────────────────────────────────────────────*/ |
|
function createButton(idSuffix = "inline") { |
|
const btn = document.createElement("button"); |
|
btn.id = `deepseek-share-btn-${idSuffix}`; |
|
btn.innerHTML = "<span style=\"font-weight:bold\"></span> Share"; |
|
btn.title = "Share this conversation as a GitHub Gist"; |
|
btn.style.cssText = |
|
"margin-left:8px;padding:4px 10px;font:14px/1.2 system-ui,sans-serif;background:#24292f;color:#fff;border:none;border-radius:4px;cursor:pointer;z-index:9999;"; |
|
|
|
btn.onclick = async () => { |
|
try { |
|
banner("Preparing conversation…", "info", 1500); |
|
const convo = scrapeConversation(); |
|
if (!convo.length) throw new Error("No messages detected on screen."); |
|
const md = toMarkdown(convo); |
|
const pat = await getGitHubPAT(); |
|
banner("Uploading Gist…", "info", 0); |
|
const url = await uploadGist(md, pat); |
|
banner("Gist created!", "success"); |
|
window.open(url, "_blank"); |
|
} catch (e) { |
|
console.error(e); |
|
banner(e.message, "error", 6000); |
|
} |
|
}; |
|
return btn; |
|
} |
|
|
|
/*───────────────────────────────────────────────*/ |
|
/* TARGET CONTAINER DETECTION */ |
|
/*───────────────────────────────────────────────*/ |
|
function findHeaderRight() { |
|
const header = document.querySelector("header"); |
|
if (!header) return null; |
|
|
|
// Choose the descendant with the right‑most edge that is a flex container |
|
const flexKids = Array.from(header.querySelectorAll("*")) |
|
.filter((n) => /flex/.test(getComputedStyle(n).display)); |
|
if (!flexKids.length) return header; |
|
return flexKids.reduce((a, b) => |
|
a.getBoundingClientRect().right > b.getBoundingClientRect().right ? a : b |
|
); |
|
} |
|
|
|
/*───────────────────────────────────────────────*/ |
|
/* INJECTION LOGIC */ |
|
/*───────────────────────────────────────────────*/ |
|
function injectButton() { |
|
if (document.querySelector("#deepseek-share-btn-inline")) return; |
|
|
|
const target = findHeaderRight(); |
|
if (target) { |
|
target.appendChild(createButton("inline")); |
|
} else if (!document.querySelector("#deepseek-share-btn-float")) { |
|
// Fallback floating button (visible even if header not found) |
|
const floatBtn = createButton("float"); |
|
floatBtn.style.position = "fixed"; |
|
floatBtn.style.top = "12px"; |
|
floatBtn.style.right = "12px"; |
|
document.body.appendChild(floatBtn); |
|
} |
|
} |
|
|
|
// Observe SPA mutations + periodic retry (covers nav changes) |
|
const obs = new MutationObserver(injectButton); |
|
obs.observe(document.body, { childList: true, subtree: true }); |
|
injectButton(); |
|
const retry = setInterval(() => { |
|
injectButton(); |
|
if (document.querySelector("#deepseek-share-btn-inline")) clearInterval(retry); |
|
}, 1500); |
|
|
|
/*───────────────────────────────────────────────*/ |
|
/* STYLES */ |
|
/*───────────────────────────────────────────────*/ |
|
GM_addStyle(` |
|
#deepseek-share-btn-inline:hover, #deepseek-share-btn-float:hover { background:#000 } |
|
`); |
|
})(); |