Skip to content

Instantly share code, notes, and snippets.

@strickvl
Created April 17, 2025 06:27
Show Gist options
  • Save strickvl/092b85d9b2bd5a7bfbc36e71f2fbc28a to your computer and use it in GitHub Desktop.
Save strickvl/092b85d9b2bd5a7bfbc36e71f2fbc28a to your computer and use it in GitHub Desktop.
ChatDeepSeek‑Chat → GitHub Gist

DeepSeek‑Chat → GitHub Gist

Tampermonkey userscript to add a “Share” button to https://chat.deepseek.com, converting the full conversation into Markdown and saving it as a private GitHub Gist.


Installation (2 mins)

  1. Install Tampermonkey in your browser.
  2. Click its toolbar icon → Create a new script → paste the contents of tampermonkey_script.js (this repo) → Save.
  3. Refresh any open DeepSeek‑Chat tab; a “ Share” button now appears top‑right.

First‑use setup

The first time you press Share the script will prompt for a GitHub Personal‑Access Token (PAT):

  1. Visit https://github.com/settings/tokens/new?scopes=gist&description=DeepSeekShare
  2. Tick the “gist” scope (✱the only permission required✱).
  3. Generate, copy, and paste the token into the prompt.
  4. The token is stored locally via Tampermonkey’s GM_setValue; never sent anywhere except GitHub.

You can update/clear the PAT later via Tampermonkey’s Storage panel or by running localStorage.removeItem('github_pat') in DevTools.


Usage

  1. Chat as usual on DeepSeek.
  2. Click  Share.
    • A status banner appears (“Preparing… → Uploading…”).
    • A new tab opens with your private Gist containing deepseek_conversation.md.

The Markdown format mirrors:

# DeepSeek Conversation

## 🧑 User---

## 🤖 Assistant---

Use GitHub’s Download ZIP or Raw to retrieve the file, or share the Gist URL.


FAQ

Does this leak my chat to any third party?

Only to GitHub, and only after you explicitly press Share. The script runs entirely in‑browser, scraping the DOM you can already see.

Can I make the Gist public?

In tampermonkey_script.js change public: false to true inside uploadGist().

Where’s the code that extracts the messages?

See scrapeConversation() in the userscript. It heuristically labels a block as assistant if it contains a .ds-markdown element; everything else is treated as user text. This was tested on DeepSeek v2025‑04‑17 and avoids any server calls.


Uninstall

Disable the userscript from Tampermonkey or simply delete it. Stored PATs can be purged via Tampermonkey’s Storage tab.


© 2025 Alex Strick van Linschoten — MIT License

// ==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 }
`);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment