mirror of
https://github.com/pionxzh/chatgpt-exporter.git
synced 2026-07-23 09:00:51 -05:00
init
This commit is contained in:
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{diff,md}]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
11
.eslintrc.cjs
Normal file
11
.eslintrc.cjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'@pionxzh/eslint-config-ts',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
},
|
||||||
|
}
|
||||||
63
.github/workflows/build.yml
vendored
Normal file
63
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
if: "contains(github.event.head_commit.message, 'chore: release')"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
|
||||||
|
- name: Use Node.js v16
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
registry-url: https://registry.npmjs.org/
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- run: pnpm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm run test
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
run: |
|
||||||
|
pnpm run clean
|
||||||
|
pnpm run release:ci
|
||||||
|
|
||||||
|
- name: Git commit
|
||||||
|
id: commit
|
||||||
|
run: |
|
||||||
|
git config --local user.email github-actions[bot]@users.noreply.github.com
|
||||||
|
git config --local user.name github-actions[bot]
|
||||||
|
git config --global core.autocrlf true
|
||||||
|
git config --global core.safecrlf false
|
||||||
|
git add .
|
||||||
|
git commit -m "CI build" -a
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Git push
|
||||||
|
uses: ad-m/github-push-action@master
|
||||||
|
if: ${{ steps.commit.outcome == 'success' }}
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
branch: ${{ github.ref }}
|
||||||
|
|
||||||
|
- name: Log
|
||||||
|
if: ${{ steps.commit.outcome != 'success' }}
|
||||||
|
run: echo Nothing to commit.
|
||||||
39
.github/workflows/check.yml
vendored
Normal file
39
.github/workflows/check.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
if: "!contains(github.event.head_commit.message, 'chore: release')"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
|
||||||
|
- name: Use Node.js v16
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
registry-url: https://registry.npmjs.org/
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- run: pnpm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm run test
|
||||||
16
.github/workflows/release-please.yml
vendored
Normal file
16
.github/workflows/release-please.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: release-please
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-please:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: google-github-actions/release-please-action@v3
|
||||||
|
with:
|
||||||
|
release-type: node
|
||||||
|
command: manifest
|
||||||
|
monorepo-tags: true
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
packages/**/dist
|
||||||
|
shared/**/dist
|
||||||
|
*.local
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm exec lint-staged
|
||||||
4
.husky/pre-push
Normal file
4
.husky/pre-push
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm run test
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022-Present Pionxzh
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
24
README.md
Normal file
24
README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
ChatGPT Exporter
|
||||||
|
==========================
|
||||||
|
|
||||||
|

|
||||||
|
[](https://greasyfork.org/scripts/456055-chatgpt-exporter)
|
||||||
|
|
||||||
|
A GreasyFork script to export the chat history of [ChatGPT](https://chatgpt.com/).
|
||||||
|
|
||||||
|
Supported Formats:
|
||||||
|
- Text
|
||||||
|
- Html
|
||||||
|
- JSON (TODO)
|
||||||
|
- GIF (TODO)
|
||||||
|
|
||||||
|
# Prerequisites
|
||||||
|
|
||||||
|
Install **Tampermonkey** **([Chrome](https://www.tampermonkey.net/)** / **[Firefox](https://addons.mozilla.org/firefox/addon/tampermonkey/)** / **[Edge](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd?hl=zh-CN))**
|
||||||
|
|
||||||
|
|
||||||
|
# UserScript
|
||||||
|
|
||||||
|
| Greasyfork | GitHub |
|
||||||
|
| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||||
|
| [Insatall](https://greasyfork.org/scripts/456055-chatgpt-exporter) | [Insatall](https://raw.githubusercontent.com/pionxzh/chatgpt-exporter/main/dist/chatgpt.user.js) |
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
37
package.json
Normal file
37
package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@pionxzh/chatgpt-exporter",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@7.12.2",
|
||||||
|
"author": "pionxzh",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "turbo run build",
|
||||||
|
"dev": "turbo run dev",
|
||||||
|
"test": "turbo run test",
|
||||||
|
"lint": "turbo run lint",
|
||||||
|
"lint:fix": "turbo run lint:fix",
|
||||||
|
"clean": "rimraf ./dist",
|
||||||
|
"release:ci": "cpy packages/**/dist/*.user.js dist --flat",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@pionxzh/eslint-config-ts": "^0.1.1",
|
||||||
|
"@types/node": "^18.11.10",
|
||||||
|
"cpy-cli": "^4.2.0",
|
||||||
|
"eslint": "^8.29.0",
|
||||||
|
"husky": "^8.0.2",
|
||||||
|
"lint-staged": "^13.1.0",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"sass": "^1.56.1",
|
||||||
|
"turbo": "^1.6.3",
|
||||||
|
"typescript": "^4.9.3"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx}": "pnpm exec eslint"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
packages/userscript/package.json
Normal file
25
packages/userscript/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@pionxzh/chatgpt-userscript",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"title": "ChatGPT Exporter",
|
||||||
|
"description": "Easily export the whole ChatGPT conversation history for further analysis or sharing.",
|
||||||
|
"author": "pionxzh",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"test:type": "tsc --noEmit",
|
||||||
|
"test": "pnpm run test:type",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"sentinel-js": "^0.0.5",
|
||||||
|
"vite-plugin-monkey": "^2.7.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^3.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/userscript/src/icons.ts
Normal file
20
packages/userscript/src/icons.ts
Normal file
File diff suppressed because one or more lines are too long
242
packages/userscript/src/main.ts
Normal file
242
packages/userscript/src/main.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import { chatGPTAvatar, chatGPTAvatarSVG, fileCode, iconCopy } from './icons'
|
||||||
|
import './style.scss'
|
||||||
|
import { copyToClipboard, downloadFile, getBase64FromImg, onloadSafe } from './utils'
|
||||||
|
import templateHtml from './template.html?raw'
|
||||||
|
|
||||||
|
appendLeftSidebarItem()
|
||||||
|
|
||||||
|
function appendLeftSidebarItem() {
|
||||||
|
onloadSafe(() => {
|
||||||
|
const firstItem = document.querySelector('[class^="Navigation__NavMenuItem"]')
|
||||||
|
if (!firstItem) return
|
||||||
|
|
||||||
|
const container = firstItem.parentElement
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
const divider = document.createElement('div')
|
||||||
|
divider.className = 'Navigation__NavMenuDivider'
|
||||||
|
container.appendChild(divider)
|
||||||
|
|
||||||
|
const copyButton = <HTMLAnchorElement>firstItem.cloneNode(true)
|
||||||
|
copyButton.removeAttribute('href')
|
||||||
|
copyButton.innerHTML = `${iconCopy}Copy`
|
||||||
|
copyButton.addEventListener('click', () => copyToText())
|
||||||
|
container.appendChild(copyButton)
|
||||||
|
|
||||||
|
// const txtButton = <HTMLAnchorElement>firstItem.cloneNode(true)
|
||||||
|
// txtButton.removeAttribute('href')
|
||||||
|
// txtButton.innerHTML = `${fileLines}Export .txt`
|
||||||
|
// container.appendChild(txtButton)
|
||||||
|
|
||||||
|
const htmlButton = <HTMLAnchorElement>firstItem.cloneNode(true)
|
||||||
|
htmlButton.removeAttribute('href')
|
||||||
|
htmlButton.innerHTML = `${fileCode}Export to .html`
|
||||||
|
htmlButton.addEventListener('click', () => exportToHtml())
|
||||||
|
container.appendChild(htmlButton)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConversationLine = |
|
||||||
|
{ type: 'text'; text: string } |
|
||||||
|
{ type: 'image'; src: string } |
|
||||||
|
{ type: 'code'; code: string } |
|
||||||
|
{ type: 'code-block'; lang: string; code: string } |
|
||||||
|
{ type: 'link'; text: string; href: string }
|
||||||
|
|
||||||
|
interface ConversationItem {
|
||||||
|
author: {
|
||||||
|
name: string
|
||||||
|
avatar: string
|
||||||
|
}
|
||||||
|
lines: ConversationLine[][]
|
||||||
|
// text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToText() {
|
||||||
|
const items = getConversation()
|
||||||
|
if (items.length === 0) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert('No conversation found. Please send a message first.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = conversationToText(items)
|
||||||
|
copyToClipboard(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportToHtml() {
|
||||||
|
const items = getConversation()
|
||||||
|
if (items.length === 0) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert('No conversation found. Please send a message first.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = document.documentElement.lang ?? 'en'
|
||||||
|
const conversationHtml = items.map((item) => {
|
||||||
|
const { author: { name, avatar }, lines } = item
|
||||||
|
|
||||||
|
const avatarEl = name === 'ChatGPT'
|
||||||
|
? `${chatGPTAvatarSVG}`
|
||||||
|
: `<img src="${avatar}" alt="${name}" />`
|
||||||
|
|
||||||
|
const linesHtml = lines.map((line) => {
|
||||||
|
const lineHtml = line.map((item) => {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'text':
|
||||||
|
return item.text
|
||||||
|
case 'image':
|
||||||
|
return `<img src="${item.src}" referrerpolicy="no-referrer" />`
|
||||||
|
case 'code':
|
||||||
|
return `<code>${item.code}</code>`
|
||||||
|
case 'code-block':
|
||||||
|
return `<pre><code class="language-${item.lang}">${item.code}</code></pre>`
|
||||||
|
case 'link':
|
||||||
|
return `<a href="${item.href}" target="_blank" rel="noopener noreferrer">${item.text}</a>`
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}).join('')
|
||||||
|
return lineHtml.startsWith('<pre>') ? lineHtml : `<p>${lineHtml}</p>`
|
||||||
|
}).join('')
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="conversation-item">
|
||||||
|
<div class="author">
|
||||||
|
${avatarEl}
|
||||||
|
</div>
|
||||||
|
<div class="conversation-content">
|
||||||
|
${linesHtml}
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
}).join('')
|
||||||
|
|
||||||
|
const html = templateHtml
|
||||||
|
.replace('{{time}}', new Date().toISOString())
|
||||||
|
.replace('{{lang}}', lang)
|
||||||
|
.replace('{{content}}', conversationHtml)
|
||||||
|
|
||||||
|
const fileName = `ChatGPT-${new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '')}.html`
|
||||||
|
downloadFile(fileName, 'text/html', html)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConversation(): ConversationItem[] {
|
||||||
|
const thread = document.querySelector('[class^="ThreadLayout__NodeWrapper"]')
|
||||||
|
if (!thread) return []
|
||||||
|
|
||||||
|
const items: ConversationItem[] = []
|
||||||
|
thread.querySelectorAll('[class^="ConversationItem__Message"]').forEach((item) => {
|
||||||
|
const avatarEl = item.querySelector<HTMLImageElement>('[class^="Avatar__Wrapper"] img:not([aria-hidden="true"])')
|
||||||
|
// actually we can get the name from the avatar's alt
|
||||||
|
// but let's keep it anonymous for privacy reasons
|
||||||
|
const name = avatarEl?.getAttribute('alt') ? 'You' : 'ChatGPT'
|
||||||
|
const avatar = avatarEl ? getBase64FromImg(avatarEl) : chatGPTAvatar
|
||||||
|
|
||||||
|
const textNode = <HTMLDivElement>item.children?.[1]?.firstChild?.firstChild
|
||||||
|
if (!textNode) return
|
||||||
|
|
||||||
|
const lines = parseTextNode(textNode)
|
||||||
|
items.push({ author: { name, avatar }, lines })
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTextNode(textNode: HTMLDivElement): ConversationLine[][] {
|
||||||
|
const children = textNode.children
|
||||||
|
if (!children || children.length === 0) {
|
||||||
|
return [[{ type: 'text', text: textNode.textContent ?? '' }]]
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: ConversationLine[][] = []
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
const child = children[i]
|
||||||
|
switch (child.tagName.toUpperCase()) {
|
||||||
|
case 'PRE': {
|
||||||
|
const codeEl = child.querySelector('code')
|
||||||
|
if (codeEl) {
|
||||||
|
const code = codeEl.textContent ?? ''
|
||||||
|
const classList = Array.from(codeEl.classList)
|
||||||
|
const lang = classList.find(c => c.startsWith('language-'))?.replace('language-', '') ?? ''
|
||||||
|
lines.push([{ type: 'code-block', lang, code }])
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'P':
|
||||||
|
default: {
|
||||||
|
// const html = child.innerHTML
|
||||||
|
// const text = html
|
||||||
|
// .replace(/<br>/g, '\r\n')
|
||||||
|
// .replace(/<code>/g, '`')
|
||||||
|
// .replace(/<\/code>/g, '`')
|
||||||
|
// // image to [image]
|
||||||
|
// .replace(/<img[^>]+>/g, '[image]')
|
||||||
|
// // link to href
|
||||||
|
// .replace(/<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g, '[$2]($1)')
|
||||||
|
|
||||||
|
const line: ConversationLine[] = []
|
||||||
|
const nodes = Array.from(child.childNodes)
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
const text = child.textContent ?? ''
|
||||||
|
line.push({ type: 'text', text })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nodes.forEach((item) => {
|
||||||
|
switch (item.nodeType) {
|
||||||
|
// code
|
||||||
|
case 1: {
|
||||||
|
// detect is it a link
|
||||||
|
if ('href' in item) {
|
||||||
|
const href = (<HTMLAnchorElement>item).getAttribute('href') ?? ''
|
||||||
|
const text = item.textContent ?? href
|
||||||
|
line.push({ type: 'link', text, href })
|
||||||
|
}
|
||||||
|
// detect is it an image
|
||||||
|
else if ((<HTMLImageElement>item).tagName?.toUpperCase() === 'IMG') {
|
||||||
|
const src = (<HTMLImageElement>item).getAttribute('src') ?? ''
|
||||||
|
line.push({ type: 'image', src })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const text = item.textContent ?? ''
|
||||||
|
line.push({ type: 'code', code: text })
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// text
|
||||||
|
case 3:
|
||||||
|
default: {
|
||||||
|
const text = item.textContent ?? ''
|
||||||
|
line.push({ type: 'text', text })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
lines.push(line)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
function conversationToText(conversation: ConversationItem[]) {
|
||||||
|
return conversation.map((item) => {
|
||||||
|
const { author: { name }, lines } = item
|
||||||
|
const text = lines.map((line) => {
|
||||||
|
return line.map((item) => {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'text': return item.text
|
||||||
|
case 'image': return '[image]'
|
||||||
|
case 'link': return `[${item.text}](${item.href})`
|
||||||
|
case 'code': return `\`${item.code}\``
|
||||||
|
case 'code-block': return `\`\`\`${item.lang}\r\n${item.code}\`\`\``
|
||||||
|
default: return ''
|
||||||
|
}
|
||||||
|
}).join('')
|
||||||
|
}).join('\r\n\r\n')
|
||||||
|
return `${name}:\r\n${text}`
|
||||||
|
}).join('\r\n\r\n')
|
||||||
|
}
|
||||||
5
packages/userscript/src/sentinel-js.d.ts
vendored
Normal file
5
packages/userscript/src/sentinel-js.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare module 'sentinel-js' {
|
||||||
|
export function on<T = HTMLElement>(cssSelectors: string | string[], callback: (el: T) => void): void
|
||||||
|
export function off<T = HTMLElement>(event: string | string[], callback: (el: T) => void): void
|
||||||
|
export function reset(): void
|
||||||
|
}
|
||||||
5
packages/userscript/src/style.scss
Normal file
5
packages/userscript/src/style.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.Navigation__NavMenuDivider {
|
||||||
|
border-bottom: 1px solid #e1e4e8;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
188
packages/userscript/src/template.html
Normal file
188
packages/userscript/src/template.html
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{lang}}" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="https://chat.openai.com/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ChatGPT Conversation</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--tw-prose-code: #111827;
|
||||||
|
--page-bg: #fff;
|
||||||
|
--page-text: #111827;
|
||||||
|
--conversation-odd-bg: rgba(247,247,248);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--tw-prose-code: #f9fafb;
|
||||||
|
--page-bg: rgba(52,53,65);
|
||||||
|
--page-text: #fff;
|
||||||
|
--conversation-odd-bg: rgb(68,70,84);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--page-text);
|
||||||
|
background-color: var(--page-bg);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .sun {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .moon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p>code {
|
||||||
|
color: var(--tw-prose-code);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: .875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p>code::before,
|
||||||
|
p>code::after {
|
||||||
|
content: "`";
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #000000;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre>code {
|
||||||
|
font-family: Söhne Mono, Monaco, Andale Mono, Ubuntu Mono, monospace !important;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: .875em;
|
||||||
|
line-height: 1.7142857;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-header .conversation-time {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-header .conversation-time-label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-header .conversation-time-value {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-header p {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-item:nth-child(odd) {
|
||||||
|
background-color: var(--conversation-odd-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
background-color: rgb(16, 163, 127);
|
||||||
|
}
|
||||||
|
|
||||||
|
.author svg {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="conversation">
|
||||||
|
<div class="conversation-header">
|
||||||
|
<h1>ChatGPT Conversation
|
||||||
|
<button class="toggle">
|
||||||
|
<svg class="sun" stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
|
||||||
|
<svg class="moon" stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</h1>
|
||||||
|
<div class="conversation-time">
|
||||||
|
<span class="conversation-time-label">Time:</span>
|
||||||
|
<span class="conversation-time-value">{{time}}</span>
|
||||||
|
</div>
|
||||||
|
<p>Generated by <a href="https://github.com/pionxzh/chatgpt-exporter">ChatGPT Exporter</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleDarkMode() {
|
||||||
|
const html = document.querySelector('html')
|
||||||
|
const isDarkMode = html.getAttribute('data-theme') === 'dark'
|
||||||
|
html.setAttribute('data-theme', isDarkMode ? 'light' : 'dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('.toggle').addEventListener('click', toggleDarkMode)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
44
packages/userscript/src/utils.ts
Normal file
44
packages/userscript/src/utils.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export function onloadSafe(fn: () => void) {
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.addEventListener('load', fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function copyToClipboard(text: string) {
|
||||||
|
try {
|
||||||
|
// modern browsers
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadFile(filename: string, type: string, content: string) {
|
||||||
|
const blob = new Blob([content], { type })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBase64FromImg(el: HTMLImageElement) {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = el.naturalWidth
|
||||||
|
canvas.height = el.naturalHeight
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return ''
|
||||||
|
ctx.drawImage(el, 0, 0)
|
||||||
|
return canvas.toDataURL('image/png')
|
||||||
|
}
|
||||||
12
packages/userscript/tsconfig.json
Normal file
12
packages/userscript/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src",
|
||||||
|
"package.json",
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
33
packages/userscript/vite.config.ts
Normal file
33
packages/userscript/vite.config.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import monkey from 'vite-plugin-monkey'
|
||||||
|
import packageJson from './package.json'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
// https://github.com/lisonge/vite-plugin-monkey/issues/10#issuecomment-1207264978
|
||||||
|
esbuild: {
|
||||||
|
charset: 'utf8',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
monkey({
|
||||||
|
entry: 'src/main.ts',
|
||||||
|
userscript: {
|
||||||
|
'name': packageJson.title,
|
||||||
|
'author': packageJson.author,
|
||||||
|
'namespace': packageJson.author,
|
||||||
|
'description': packageJson.description,
|
||||||
|
'license': packageJson.license,
|
||||||
|
'match': 'https://chat.openai.com/chat',
|
||||||
|
'icon': 'https://chat.openai.com/favicon.ico',
|
||||||
|
'run-at': 'document-start',
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
fileName: 'chatgpt.user.js',
|
||||||
|
minifyCss: false,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
3149
pnpm-lock.yaml
generated
Normal file
3149
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
- 'packages/*'
|
||||||
9
release-please-config.json
Normal file
9
release-please-config.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"pull-request-header": ":robot: I have created a release",
|
||||||
|
"pull-request-title-pattern": "chore: release${component} ${version}",
|
||||||
|
"packages": {
|
||||||
|
"packages/exporter": {
|
||||||
|
"component": "exporter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
tsconfig.base.json
Normal file
26
tsconfig.base.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"useDefineForClassFields": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"**/dist",
|
||||||
|
"**/.turbo"
|
||||||
|
]
|
||||||
|
}
|
||||||
23
turbo.json
Normal file
23
turbo.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://turborepo.org/schema.json",
|
||||||
|
"pipeline": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": ["dist/**"]
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": ["build"],
|
||||||
|
"outputs": [],
|
||||||
|
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"]
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
"lint:fix": {
|
||||||
|
"outputs": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user