├── src ├── vite-env.d.ts ├── assets │ ├── gemini.gif │ ├── copied.svg │ ├── copy.svg │ ├── chatbot.svg │ └── openai.svg ├── style.css ├── utils │ ├── types.ts │ └── index.ts ├── main.ts ├── Print.vue ├── components │ └── ConfigDialog.vue ├── App.vue └── markdown.css ├── public ├── logo.png └── logo.svg ├── .vscode └── extensions.json ├── tsconfig.node.json ├── .gitignore ├── index.html ├── tsconfig.json ├── README.md ├── package.json ├── electron ├── electron-env.d.ts ├── updater.ts ├── database.ts ├── preload.ts └── main.ts ├── LICENSE ├── electron-builder.json5 ├── .github └── workflows │ └── release.yml └── vite.config.ts /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenyikun/chat-electron-app/HEAD/public/logo.png -------------------------------------------------------------------------------- /src/assets/gemini.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenyikun/chat-electron-app/HEAD/src/assets/gemini.gif -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | } 5 | 6 | ul { 7 | margin: 0; 8 | padding: 0; 9 | list-style: none; 10 | } -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface FileDetail { 2 | data: string 3 | mimeType: string 4 | } 5 | 6 | export interface MessageType { 7 | role: 'user' | 'assistant' 8 | type?: 'openai' | 'gemini' 9 | content: string 10 | files?: FileDetail[] 11 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/copied.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | dist-electron 14 | release 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GemChat 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function copyText(text: string) { 2 | const input = document.createElement('textarea') 3 | input.value = text 4 | input.setAttribute('style', 'position: absolute; left: -9999px;') 5 | document.body.appendChild(input) 6 | input.select() 7 | document.execCommand('copy') 8 | document.body.removeChild(input) 9 | } 10 | 11 | // 文件转成base64 12 | export async function fileToGenerativePart(file: File) { 13 | const base64EncodedDataPromise = new Promise((resolve) => { 14 | const reader = new FileReader() 15 | reader.onloadend = () => resolve((reader.result as string).split(',')[1]) 16 | reader.readAsDataURL(file) 17 | }) 18 | return { data: await base64EncodedDataPromise, mimeType: file.type } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "electron"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import 'virtual:uno.css' 3 | import './style.css' 4 | import 'primevue/resources/themes/lara-dark-green/theme.css' 5 | import 'primeicons/primeicons.css' 6 | import App from './App.vue' 7 | import Print from './Print.vue' 8 | import PrimeVue from 'primevue/config' 9 | import Ripple from 'primevue/ripple' 10 | import Tooltip from 'primevue/tooltip' 11 | import ToastService from 'primevue/toastservice' 12 | 13 | const app = createApp(location.hash === '#print' ? Print : App) 14 | app.use(PrimeVue, { ripple: true }) 15 | app.directive('ripple', Ripple) 16 | app.directive('tooltip', Tooltip) 17 | app.use(ToastService) 18 | app.mount('#app').$nextTick(() => { 19 | // Remove Preload scripts loading 20 | postMessage({ payload: 'removeLoading' }, '*') 21 | }) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Super ChatGPT 2 | 3 | A ChatGPT chat application developed using Electron. 4 | 5 | ![](https://res.vekun.com/uploads/1-1702261876343.png?imageMogr2/thumbnail/!40p) 6 | 7 | ## Proxy Host 8 | 9 | | 服务商 | 源 HOST | 可用代理 HOST | 10 | | ------ | ----------------------------------------- | -------------------------------------------------- | 11 | | OpenAI | https://api.openai.com | https://openhi.deno.dev
https://openhi.onetry.top | 12 | | Gemini | https://generativelanguage.googleapis.com | https://playai.deno.dev
https://playai.onetry.top | 13 | 14 | ## Development 15 | 16 | ``` 17 | git clone https://github.com/wenyikun/chat-electron-app.git 18 | cd chat-electron-app 19 | npm install 20 | npm run dev 21 | ``` 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-electron-app", 3 | "private": true, 4 | "version": "0.4.1", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc && vite build && electron-builder", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@bytemd/plugin-gfm": "^1.21.0", 12 | "@bytemd/plugin-highlight": "^1.21.0", 13 | "@bytemd/plugin-math": "^1.21.0", 14 | "@bytemd/vue-next": "^1.21.0", 15 | "electron-updater": "^6.1.7", 16 | "knex": "^3.0.1", 17 | "localforage": "^1.10.0", 18 | "primeicons": "^6.0.1", 19 | "primevue": "^3.41.1", 20 | "sqlite3": "^5.1.6", 21 | "vue": "^3.3.4" 22 | }, 23 | "devDependencies": { 24 | "@vitejs/plugin-vue": "^4.3.4", 25 | "electron": "^26.1.0", 26 | "electron-builder": "^24.6.4", 27 | "typescript": "^5.2.2", 28 | "unocss": "^0.57.7", 29 | "vite": "^4.4.9", 30 | "vite-plugin-electron": "^0.14.0", 31 | "vite-plugin-electron-renderer": "^0.14.5", 32 | "vue-tsc": "^1.8.8" 33 | }, 34 | "main": "dist-electron/main.js" 35 | } 36 | -------------------------------------------------------------------------------- /electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | /** 6 | * The built directory structure 7 | * 8 | * ```tree 9 | * ├─┬─┬ dist 10 | * │ │ └── index.html 11 | * │ │ 12 | * │ ├─┬ dist-electron 13 | * │ │ ├── main.js 14 | * │ │ └── preload.js 15 | * │ 16 | * ``` 17 | */ 18 | DIST: string 19 | /** /dist/ or /public/ */ 20 | VITE_PUBLIC: string 21 | } 22 | } 23 | 24 | // Used in Renderer process, expose in `preload.ts` 25 | interface Window { 26 | db: { 27 | insertConversation: (data: any) => Promise 28 | getConversations: (data: { pageSize: number; pageNum: number; }) => Promise 29 | updateConversation: (data: any) => Promise 30 | getConversation: (id: string) => Promise 31 | deleteConversation: (id: string) => Promise 32 | }, 33 | toolApi: { 34 | printToPDF: () => Promise 35 | openPrintView: (messages: any[]) => void 36 | getPrintMessages: () => Promise 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Wen Yikun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /electron/updater.ts: -------------------------------------------------------------------------------- 1 | import { dialog } from 'electron' 2 | import { autoUpdater } from 'electron-updater' 3 | // import log from 'electron-log' 4 | 5 | // log.transports.file.level = "debug" 6 | // autoUpdater.logger = log 7 | 8 | autoUpdater.autoDownload = false 9 | 10 | autoUpdater.on('error', (error) => { 11 | dialog.showErrorBox('Error: ', error == null ? 'unknown' : (error.stack || error).toString()) 12 | }) 13 | 14 | autoUpdater.on('update-not-available', () => { 15 | dialog.showMessageBox({ 16 | title: '软件无更新', 17 | message: '当前已是最新版本,无需更新。' 18 | }) 19 | }) 20 | 21 | autoUpdater.on('update-available', () => { 22 | dialog 23 | .showMessageBox({ 24 | type: 'info', 25 | title: '软件更新', 26 | message: '有新版本可更新,是否现在更新?', 27 | buttons: ['立即更新', '暂不更新'], 28 | }) 29 | .then(() => { 30 | autoUpdater.downloadUpdate() 31 | }) 32 | }) 33 | 34 | autoUpdater.on('update-downloaded', () => { 35 | dialog.showMessageBox({ 36 | title: '下载完成', 37 | message: '更新包已下载, 程序将会退出更新...' 38 | }).then(() => { 39 | setImmediate(() => autoUpdater.quitAndInstall()) 40 | }) 41 | }) 42 | 43 | export function checkForUpdates() { 44 | autoUpdater.checkForUpdates() 45 | } 46 | -------------------------------------------------------------------------------- /electron-builder.json5: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://www.electron.build/configuration/configuration 3 | */ 4 | { 5 | $schema: 'https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json', 6 | appId: 'YourAppID', 7 | asar: true, 8 | productName: 'GemChat', 9 | directories: { 10 | output: 'release/${version}', 11 | }, 12 | files: ['dist', 'dist-electron'], 13 | mac: { 14 | target: ['dmg'], 15 | icon: 'public/logo.png', 16 | artifactName: '${productName}-Mac-${version}-Installer.${ext}', 17 | }, 18 | win: { 19 | target: [ 20 | { 21 | target: 'nsis', 22 | arch: ['x64'], 23 | }, 24 | ], 25 | icon: 'public/logo.png', 26 | artifactName: '${productName}-Windows-${version}-Setup.${ext}', 27 | }, 28 | nsis: { 29 | oneClick: false, 30 | perMachine: false, 31 | allowToChangeInstallationDirectory: true, 32 | deleteAppDataOnUninstall: false, 33 | }, 34 | linux: { 35 | target: ['AppImage'], 36 | icon: 'public/logo.png', 37 | artifactName: '${productName}-Linux-${version}.${ext}', 38 | }, 39 | publish: { 40 | provider: 'github', 41 | owner: 'wenyikun', 42 | repo: 'chat-electron-app', 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /electron/database.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | import path from 'node:path' 3 | const knex = require('knex') 4 | 5 | // 数据库配置函数 6 | function configureDatabase() { 7 | // 获取用户数据目录的路径 8 | const userDataPath = app.getPath('userData') 9 | // 构建数据库文件的完整路径 10 | const dbPath = path.join(userDataPath, 'local.db') 11 | console.log('Database path:', dbPath) 12 | 13 | return knex({ 14 | client: 'sqlite3', 15 | connection: { 16 | filename: dbPath, 17 | }, 18 | useNullAsDefault: true, 19 | }) 20 | } 21 | 22 | // 初始化数据库表 23 | async function initTableConversations(knex: any) { 24 | const exists = await knex.schema.hasTable('conversations') 25 | if (!exists) { 26 | await knex.schema.createTable('conversations', function (table: any) { 27 | table.increments('id').primary() 28 | // table.string('chat_id') 29 | table.string('title') 30 | table.json('conversations') 31 | table.integer('created_at').defaultTo(Date.now()) 32 | }) 33 | } 34 | } 35 | 36 | export default (async function setupDatabase() { 37 | try { 38 | const knex = configureDatabase() 39 | await initTableConversations(knex) 40 | return knex 41 | } catch (error) { 42 | console.error('Database setup failed:', error) 43 | throw error // 或者处理错误 44 | } 45 | })() 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [macos-latest, windows-latest] 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v3 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 18 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Build Electron package 28 | run: npm run build 29 | env: 30 | GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} 31 | 32 | release: 33 | needs: build 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout code 37 | uses: actions/checkout@v3 38 | 39 | - name: Publish release 40 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 41 | run: gh release edit ${{ github.ref_name }} --draft=false 42 | env: 43 | GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} 44 | 45 | # - name: Create release 46 | # uses: softprops/action-gh-release@v1 47 | # if: startsWith(github.ref, 'refs/tags/') 48 | # with: 49 | # token: ${{ secrets.RELEASE_TOKEN }} 50 | # files: | 51 | # ./release/${{ github.ref }}/GemChat-Windows-${{ github.ref }}-Setup.exe 52 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import path from 'node:path' 3 | import electron from 'vite-plugin-electron/simple' 4 | import vue from '@vitejs/plugin-vue' 5 | import UnoCSS from 'unocss/vite' 6 | import presetUno from '@unocss/preset-uno' 7 | import presetAttributify from '@unocss/preset-attributify' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | vue(), 13 | electron({ 14 | main: { 15 | // Shortcut of `build.lib.entry`. 16 | entry: 'electron/main.ts', 17 | }, 18 | preload: { 19 | // Shortcut of `build.rollupOptions.input`. 20 | // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`. 21 | input: path.join(__dirname, 'electron/preload.ts'), 22 | }, 23 | // Ployfill the Electron and Node.js built-in modules for Renderer process. 24 | // See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer 25 | renderer: {}, 26 | }), 27 | UnoCSS({ 28 | presets: [ 29 | presetUno(), 30 | presetAttributify() 31 | ], 32 | content: { 33 | pipeline: { 34 | include: ["./index.html", 35 | "./src/**/*.{vue,js,ts,jsx,tsx}", 36 | "./node_modules/primevue/**/*.{vue,js,ts,jsx,tsx}"] 37 | } 38 | } 39 | }) 40 | ], 41 | }) 42 | -------------------------------------------------------------------------------- /src/Print.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 63 | -------------------------------------------------------------------------------- /src/assets/chatbot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /electron/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | 3 | // 数据库操作 api 4 | contextBridge.exposeInMainWorld('db', { 5 | insertConversation: (data: any) => ipcRenderer.invoke('insertConversation', data), 6 | getConversations: (data: { pageSize: number; pageNum: number; } = { pageSize: 10, pageNum: 1 }) => ipcRenderer.invoke('getConversations', data), 7 | updateConversation: (data: any) => ipcRenderer.invoke('updateConversation', data), 8 | getConversation: (id: number) => ipcRenderer.invoke('getConversation', id), 9 | deleteConversation: (id: number) => ipcRenderer.invoke('deleteConversation', id) 10 | }) 11 | 12 | contextBridge.exposeInMainWorld('toolApi', { 13 | printToPDF: () => ipcRenderer.invoke('printToPDF'), 14 | openPrintView: (messages: any[]) => { 15 | ipcRenderer.send('openPrintView', messages) 16 | }, 17 | getPrintMessages: () => ipcRenderer.invoke('getPrintMessages'), 18 | }) 19 | 20 | // --------- Preload scripts loading --------- 21 | function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) { 22 | return new Promise((resolve) => { 23 | if (condition.includes(document.readyState)) { 24 | resolve(true) 25 | } else { 26 | document.addEventListener('readystatechange', () => { 27 | if (condition.includes(document.readyState)) { 28 | resolve(true) 29 | } 30 | }) 31 | } 32 | }) 33 | } 34 | 35 | const safeDOM = { 36 | append(parent: HTMLElement, child: HTMLElement) { 37 | if (!Array.from(parent.children).find((e) => e === child)) { 38 | parent.appendChild(child) 39 | } 40 | }, 41 | remove(parent: HTMLElement, child: HTMLElement) { 42 | if (Array.from(parent.children).find((e) => e === child)) { 43 | parent.removeChild(child) 44 | } 45 | }, 46 | } 47 | 48 | /** 49 | * https://tobiasahlin.com/spinkit 50 | * https://connoratherton.com/loaders 51 | * https://projects.lukehaas.me/css-loaders 52 | * https://matejkustec.github.io/SpinThatShit 53 | */ 54 | function useLoading() { 55 | const className = `loaders-css__square-spin` 56 | const styleContent = ` 57 | @keyframes square-spin { 58 | 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } 59 | 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); } 60 | 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); } 61 | 100% { transform: perspective(100px) rotateX(0) rotateY(0); } 62 | } 63 | .${className} > div { 64 | animation-fill-mode: both; 65 | width: 50px; 66 | height: 50px; 67 | background: #fff; 68 | animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite; 69 | } 70 | .app-loading-wrap { 71 | position: fixed; 72 | top: 0; 73 | left: 0; 74 | width: 100vw; 75 | height: 100vh; 76 | display: flex; 77 | align-items: center; 78 | justify-content: center; 79 | background: #282c34; 80 | z-index: 9; 81 | } 82 | ` 83 | const oStyle = document.createElement('style') 84 | const oDiv = document.createElement('div') 85 | 86 | oStyle.id = 'app-loading-style' 87 | oStyle.innerHTML = styleContent 88 | oDiv.className = 'app-loading-wrap' 89 | oDiv.innerHTML = `
` 90 | 91 | return { 92 | appendLoading() { 93 | safeDOM.append(document.head, oStyle) 94 | safeDOM.append(document.body, oDiv) 95 | }, 96 | removeLoading() { 97 | safeDOM.remove(document.head, oStyle) 98 | safeDOM.remove(document.body, oDiv) 99 | }, 100 | } 101 | } 102 | 103 | // ---------------------------------------------------------------------- 104 | 105 | const { appendLoading, removeLoading } = useLoading() 106 | domReady().then(appendLoading) 107 | 108 | window.onmessage = (ev) => { 109 | ev.data.payload === 'removeLoading' && removeLoading() 110 | } 111 | 112 | setTimeout(removeLoading, 4999) 113 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/openai.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/components/ConfigDialog.vue: -------------------------------------------------------------------------------- 1 | 137 | 138 | 159 | -------------------------------------------------------------------------------- /electron/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron' 2 | import path from 'node:path' 3 | import fs from 'node:fs' 4 | import database from './database' 5 | // import { checkForUpdates } from './updater' 6 | // The built directory structure 7 | // 8 | // ├─┬─┬ dist 9 | // │ │ └── index.html 10 | // │ │ 11 | // │ ├─┬ dist-electron 12 | // │ │ ├── main.js 13 | // │ │ └── preload.js 14 | // │ 15 | app.setName('GemChat') // 设置当前应用程序的名字 16 | process.env.DIST = path.join(__dirname, '../dist') 17 | process.env.VITE_PUBLIC = app.isPackaged ? process.env.DIST : path.join(process.env.DIST, '../public') 18 | 19 | let win: BrowserWindow | null 20 | // 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x 21 | const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'] 22 | 23 | function createWindow() { 24 | win = new BrowserWindow({ 25 | icon: path.join(process.env.VITE_PUBLIC, 'logo.svg'), 26 | webPreferences: { 27 | preload: path.join(__dirname, 'preload.js'), 28 | // devTools: !!VITE_DEV_SERVER_URL 29 | }, 30 | }) 31 | 32 | // 拦截跳转并用浏览器打开 33 | win.webContents.on('will-navigate', (event) => { 34 | shell.openExternal(event.url) 35 | event.preventDefault() 36 | }) 37 | 38 | if (VITE_DEV_SERVER_URL) { 39 | win.loadURL(VITE_DEV_SERVER_URL) 40 | } else { 41 | // win.loadFile('dist/index.html') 42 | win.loadFile(path.join(process.env.DIST, 'index.html')) 43 | } 44 | } 45 | 46 | // Quit when all windows are closed, except on macOS. There, it's common 47 | // for applications and their menu bar to stay active until the user quits 48 | // explicitly with Cmd + Q. 49 | app.on('window-all-closed', () => { 50 | if (process.platform !== 'darwin') { 51 | app.quit() 52 | win = null 53 | } 54 | }) 55 | 56 | app.on('activate', () => { 57 | // On OS X it's common to re-create a window in the app when the 58 | // dock icon is clicked and there are no other windows open. 59 | if (BrowserWindow.getAllWindows().length === 0) { 60 | createWindow() 61 | } 62 | }) 63 | 64 | app.whenReady().then(() => { 65 | // 对话插入 66 | ipcMain.handle('insertConversation', (_event, data) => database.then((db) => db.insert(data).into('conversations'))) 67 | ipcMain.handle('getConversations', (_event, data) => { 68 | return database.then((db) => 69 | db 70 | .select('id', 'title') 71 | .limit(data.pageSize) 72 | .offset((data.pageNum - 1) * data.pageSize) 73 | .from('conversations') 74 | .orderBy('id', 'desc') 75 | ) 76 | }) 77 | ipcMain.handle('updateConversation', (_event, data) => 78 | database.then((db) => db.update(data).into('conversations').where({ id: data.id })) 79 | ) 80 | ipcMain.handle('getConversation', (_event, id) => 81 | database.then((db) => db.from('conversations').where('id', id).first()) 82 | ) 83 | ipcMain.handle('deleteConversation', (_event, id) => 84 | database.then((db) => db.delete().from('conversations').where('id', id)) 85 | ) 86 | 87 | let pdfWin: BrowserWindow | null 88 | let pdfMessages: any[] = [] 89 | const closePdfWin = () => { 90 | pdfWin?.close() 91 | pdfWin = null 92 | pdfMessages = [] 93 | } 94 | ipcMain.on('openPrintView', (_event, messages) => { 95 | pdfMessages = messages 96 | pdfWin = new BrowserWindow({ 97 | icon: path.join(process.env.VITE_PUBLIC, 'logo.svg'), 98 | webPreferences: { 99 | preload: path.join(__dirname, 'preload.js'), 100 | }, 101 | show: false, 102 | }) 103 | 104 | if (VITE_DEV_SERVER_URL) { 105 | pdfWin.loadURL(VITE_DEV_SERVER_URL + '#print') 106 | } else { 107 | // win.loadFile('dist/index.html') 108 | pdfWin.loadFile(path.join(process.env.DIST, 'index.html'), { 109 | hash: 'print', 110 | }) 111 | } 112 | }) 113 | 114 | ipcMain.handle('getPrintMessages', () => pdfMessages) 115 | 116 | ipcMain.handle('printToPDF', async (event) => { 117 | try { 118 | const value = await dialog.showSaveDialog({ 119 | title: '保存为PDF', 120 | defaultPath: '未命名.pdf', 121 | filters: [{ name: 'PDF', extensions: ['pdf'] }], 122 | }) 123 | if (value.canceled) { 124 | closePdfWin() 125 | return 126 | } 127 | const data = await event.sender.printToPDF({ 128 | printBackground: true, 129 | margins: { 130 | left: 0, 131 | top: 0, 132 | right: 0, 133 | bottom: 0, 134 | }, 135 | // scale: 1.2, 136 | landscape: true, 137 | pageSize: 'A4', 138 | }) 139 | await fs.promises.writeFile(value.filePath as string, data) 140 | closePdfWin() 141 | } catch (error) { 142 | closePdfWin() 143 | } 144 | }) 145 | 146 | // 添加一个检查更新按钮 147 | // const template = Menu.buildFromTemplate([ 148 | // { 149 | // label: 'Check for Updates', 150 | // click: () => checkForUpdates(), 151 | // }, 152 | // ]) 153 | // const appMenu = Menu.getApplicationMenu() 154 | // appMenu?.items[0].submenu?.insert(1, template.items[0]) // 在第二个位置插入新菜单项 155 | // Menu.setApplicationMenu(appMenu) 156 | 157 | createWindow() 158 | }) 159 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 468 | 469 | 619 | 620 | 621 | -------------------------------------------------------------------------------- /src/markdown.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --success-color: #10a37f; 3 | --main-font-size: 1rem; 4 | } 5 | 6 | .hljs { 7 | border-radius: 6px; 8 | } 9 | 10 | .markdown-body { 11 | -ms-text-size-adjust: 100%; 12 | -webkit-text-size-adjust: 100%; 13 | width: 100%; 14 | margin: 0; 15 | font-size: var(--main-font-size); 16 | line-height: 1.5rem; 17 | word-wrap: break-word; 18 | overflow-x: auto; 19 | } 20 | 21 | .markdown-body .octicon { 22 | display: inline-block; 23 | fill: currentColor; 24 | vertical-align: text-bottom; 25 | } 26 | 27 | .markdown-body h1:hover .anchor .octicon-link:before, 28 | .markdown-body h2:hover .anchor .octicon-link:before, 29 | .markdown-body h3:hover .anchor .octicon-link:before, 30 | .markdown-body h4:hover .anchor .octicon-link:before, 31 | .markdown-body h5:hover .anchor .octicon-link:before, 32 | .markdown-body h6:hover .anchor .octicon-link:before { 33 | width: 16px; 34 | height: 16px; 35 | content: ' '; 36 | display: inline-block; 37 | background-color: currentColor; 38 | -webkit-mask-image: url("data:image/svg+xml,"); 39 | mask-image: url("data:image/svg+xml,"); 40 | } 41 | 42 | .markdown-body details, 43 | .markdown-body figcaption, 44 | .markdown-body figure { 45 | display: block; 46 | } 47 | 48 | .markdown-body summary { 49 | display: list-item; 50 | } 51 | 52 | .markdown-body [hidden] { 53 | display: none !important; 54 | } 55 | 56 | .markdown-body a { 57 | background-color: transparent; 58 | color: #58a6ff; 59 | text-decoration: none; 60 | } 61 | 62 | .markdown-body abbr[title] { 63 | border-bottom: none; 64 | text-decoration: underline dotted; 65 | } 66 | 67 | .markdown-body b, 68 | .markdown-body strong { 69 | font-weight: 600; 70 | } 71 | 72 | .markdown-body dfn { 73 | font-style: italic; 74 | } 75 | 76 | .markdown-body h1 { 77 | margin: .67em 0; 78 | font-weight: 600; 79 | padding-bottom: .3em; 80 | font-size: 2em; 81 | border-bottom: 1px solid #21262d; 82 | } 83 | 84 | .markdown-body mark { 85 | background-color: rgba(187, 128, 9, 0.15); 86 | color: #c9d1d9; 87 | } 88 | 89 | .markdown-body small { 90 | font-size: 90%; 91 | } 92 | 93 | .markdown-body sub, 94 | .markdown-body sup { 95 | font-size: 75%; 96 | line-height: 0; 97 | position: relative; 98 | vertical-align: baseline; 99 | } 100 | 101 | .markdown-body sub { 102 | bottom: -0.25em; 103 | } 104 | 105 | .markdown-body sup { 106 | top: -0.5em; 107 | } 108 | 109 | .markdown-body img { 110 | border-style: none; 111 | max-width: 100%; 112 | box-sizing: content-box; 113 | } 114 | 115 | .markdown-body code, 116 | .markdown-body kbd, 117 | .markdown-body pre, 118 | .markdown-body samp { 119 | font-family: monospace; 120 | font-size: 1em; 121 | } 122 | 123 | .markdown-body figure { 124 | margin: 1em 40px; 125 | } 126 | 127 | .markdown-body hr { 128 | box-sizing: content-box; 129 | overflow: hidden; 130 | background: transparent; 131 | border-bottom: 1px solid #21262d; 132 | height: .25em; 133 | padding: 0; 134 | margin: 24px 0; 135 | background-color: #30363d; 136 | border: 0; 137 | } 138 | 139 | .markdown-body input { 140 | font: inherit; 141 | margin: 0; 142 | overflow: visible; 143 | font-family: inherit; 144 | font-size: inherit; 145 | line-height: inherit; 146 | } 147 | 148 | .markdown-body [type=button], 149 | .markdown-body [type=reset], 150 | .markdown-body [type=submit] { 151 | -webkit-appearance: button; 152 | } 153 | 154 | .markdown-body [type=checkbox], 155 | .markdown-body [type=radio] { 156 | box-sizing: border-box; 157 | padding: 0; 158 | } 159 | 160 | .markdown-body [type=number]::-webkit-inner-spin-button, 161 | .markdown-body [type=number]::-webkit-outer-spin-button { 162 | height: auto; 163 | } 164 | 165 | .markdown-body [type=search]::-webkit-search-cancel-button, 166 | .markdown-body [type=search]::-webkit-search-decoration { 167 | -webkit-appearance: none; 168 | } 169 | 170 | .markdown-body ::-webkit-input-placeholder { 171 | color: inherit; 172 | opacity: .54; 173 | } 174 | 175 | .markdown-body ::-webkit-file-upload-button { 176 | -webkit-appearance: button; 177 | font: inherit; 178 | } 179 | 180 | .markdown-body a:hover { 181 | text-decoration: underline; 182 | } 183 | 184 | .markdown-body ::placeholder { 185 | color: #6e7681; 186 | opacity: 1; 187 | } 188 | 189 | .markdown-body hr::before { 190 | display: table; 191 | content: ""; 192 | } 193 | 194 | .markdown-body hr::after { 195 | display: table; 196 | clear: both; 197 | content: ""; 198 | } 199 | 200 | .markdown-body table { 201 | border-spacing: 0; 202 | border-collapse: collapse; 203 | display: block; 204 | width: max-content; 205 | max-width: 100%; 206 | overflow: auto; 207 | } 208 | 209 | .markdown-body td, 210 | .markdown-body th { 211 | padding: 0; 212 | } 213 | 214 | .markdown-body details summary { 215 | cursor: pointer; 216 | } 217 | 218 | .markdown-body details:not([open])>*:not(summary) { 219 | display: none !important; 220 | } 221 | 222 | .markdown-body a:focus, 223 | .markdown-body [role=button]:focus, 224 | .markdown-body input[type=radio]:focus, 225 | .markdown-body input[type=checkbox]:focus { 226 | outline: 2px solid #58a6ff; 227 | outline-offset: -2px; 228 | box-shadow: none; 229 | } 230 | 231 | .markdown-body a:focus:not(:focus-visible), 232 | .markdown-body [role=button]:focus:not(:focus-visible), 233 | .markdown-body input[type=radio]:focus:not(:focus-visible), 234 | .markdown-body input[type=checkbox]:focus:not(:focus-visible) { 235 | outline: solid 1px transparent; 236 | } 237 | 238 | .markdown-body a:focus-visible, 239 | .markdown-body [role=button]:focus-visible, 240 | .markdown-body input[type=radio]:focus-visible, 241 | .markdown-body input[type=checkbox]:focus-visible { 242 | outline: 2px solid #58a6ff; 243 | outline-offset: -2px; 244 | box-shadow: none; 245 | } 246 | 247 | .markdown-body a:not([class]):focus, 248 | .markdown-body a:not([class]):focus-visible, 249 | .markdown-body input[type=radio]:focus, 250 | .markdown-body input[type=radio]:focus-visible, 251 | .markdown-body input[type=checkbox]:focus, 252 | .markdown-body input[type=checkbox]:focus-visible { 253 | outline-offset: 0; 254 | } 255 | 256 | .markdown-body kbd { 257 | display: inline-block; 258 | padding: 3px 5px; 259 | font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 260 | line-height: 10px; 261 | color: #c9d1d9; 262 | vertical-align: middle; 263 | background-color: #161b22; 264 | border: solid 1px rgba(110, 118, 129, 0.4); 265 | border-bottom-color: rgba(110, 118, 129, 0.4); 266 | border-radius: 6px; 267 | box-shadow: inset 0 -1px 0 rgba(110, 118, 129, 0.4); 268 | } 269 | 270 | .markdown-body h1, 271 | .markdown-body h2, 272 | .markdown-body h3, 273 | .markdown-body h4, 274 | .markdown-body h5, 275 | .markdown-body h6 { 276 | margin-top: 24px; 277 | margin-bottom: 16px; 278 | font-weight: 600; 279 | line-height: 1.25; 280 | } 281 | 282 | .markdown-body h2 { 283 | font-weight: 600; 284 | padding-bottom: .3em; 285 | font-size: 1.5em; 286 | border-bottom: 1px solid #21262d; 287 | } 288 | 289 | .markdown-body h3 { 290 | font-weight: 600; 291 | font-size: 1.25em; 292 | } 293 | 294 | .markdown-body h4 { 295 | font-weight: 600; 296 | font-size: 1em; 297 | } 298 | 299 | .markdown-body h5 { 300 | font-weight: 600; 301 | font-size: .875em; 302 | } 303 | 304 | .markdown-body h6 { 305 | font-weight: 600; 306 | font-size: .85em; 307 | color: #8b949e; 308 | } 309 | 310 | .markdown-body p { 311 | margin-top: 0; 312 | margin-bottom: 10px; 313 | } 314 | 315 | .markdown-body blockquote { 316 | margin: 0; 317 | padding: 0 1em; 318 | color: #8b949e; 319 | border-left: .25em solid #30363d; 320 | } 321 | 322 | .markdown-body ul, 323 | .markdown-body ol { 324 | margin-top: 0; 325 | margin-bottom: 0; 326 | padding-left: 2em; 327 | } 328 | 329 | .markdown-body ol ol, 330 | .markdown-body ul ol { 331 | list-style-type: lower-roman; 332 | } 333 | 334 | .markdown-body ul ul ol, 335 | .markdown-body ul ol ol, 336 | .markdown-body ol ul ol, 337 | .markdown-body ol ol ol { 338 | list-style-type: lower-alpha; 339 | } 340 | 341 | .markdown-body dd { 342 | margin-left: 0; 343 | } 344 | 345 | .markdown-body tt, 346 | .markdown-body code, 347 | .markdown-body samp { 348 | font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 349 | font-size: 12px; 350 | } 351 | 352 | .markdown-body pre { 353 | margin-top: 0; 354 | margin-bottom: 0; 355 | font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 356 | font-size: 12px; 357 | word-wrap: normal; 358 | } 359 | 360 | .markdown-body .octicon { 361 | display: inline-block; 362 | overflow: visible !important; 363 | vertical-align: text-bottom; 364 | fill: currentColor; 365 | } 366 | 367 | .markdown-body input::-webkit-outer-spin-button, 368 | .markdown-body input::-webkit-inner-spin-button { 369 | margin: 0; 370 | -webkit-appearance: none; 371 | appearance: none; 372 | } 373 | 374 | .markdown-body::before { 375 | display: table; 376 | content: ""; 377 | } 378 | 379 | .markdown-body::after { 380 | display: table; 381 | clear: both; 382 | content: ""; 383 | } 384 | 385 | .markdown-body>*:first-child { 386 | margin-top: 0 !important; 387 | } 388 | 389 | .markdown-body>*:last-child { 390 | margin-bottom: 0 !important; 391 | } 392 | 393 | .markdown-body a:not([href]) { 394 | color: inherit; 395 | text-decoration: none; 396 | } 397 | 398 | .markdown-body .absent { 399 | color: #f85149; 400 | } 401 | 402 | .markdown-body .anchor { 403 | float: left; 404 | padding-right: 4px; 405 | margin-left: -20px; 406 | line-height: 1; 407 | } 408 | 409 | .markdown-body .anchor:focus { 410 | outline: none; 411 | } 412 | 413 | .markdown-body p, 414 | .markdown-body blockquote, 415 | .markdown-body ul, 416 | .markdown-body ol, 417 | .markdown-body dl, 418 | .markdown-body table, 419 | .markdown-body pre, 420 | .markdown-body details { 421 | margin-top: 0; 422 | margin-bottom: 16px; 423 | } 424 | 425 | .markdown-body blockquote>:first-child { 426 | margin-top: 0; 427 | } 428 | 429 | .markdown-body blockquote>:last-child { 430 | margin-bottom: 0; 431 | } 432 | 433 | .markdown-body h1 .octicon-link, 434 | .markdown-body h2 .octicon-link, 435 | .markdown-body h3 .octicon-link, 436 | .markdown-body h4 .octicon-link, 437 | .markdown-body h5 .octicon-link, 438 | .markdown-body h6 .octicon-link { 439 | color: #c9d1d9; 440 | vertical-align: middle; 441 | visibility: hidden; 442 | } 443 | 444 | .markdown-body h1:hover .anchor, 445 | .markdown-body h2:hover .anchor, 446 | .markdown-body h3:hover .anchor, 447 | .markdown-body h4:hover .anchor, 448 | .markdown-body h5:hover .anchor, 449 | .markdown-body h6:hover .anchor { 450 | text-decoration: none; 451 | } 452 | 453 | .markdown-body h1:hover .anchor .octicon-link, 454 | .markdown-body h2:hover .anchor .octicon-link, 455 | .markdown-body h3:hover .anchor .octicon-link, 456 | .markdown-body h4:hover .anchor .octicon-link, 457 | .markdown-body h5:hover .anchor .octicon-link, 458 | .markdown-body h6:hover .anchor .octicon-link { 459 | visibility: visible; 460 | } 461 | 462 | .markdown-body h1 tt, 463 | .markdown-body h1 code, 464 | .markdown-body h2 tt, 465 | .markdown-body h2 code, 466 | .markdown-body h3 tt, 467 | .markdown-body h3 code, 468 | .markdown-body h4 tt, 469 | .markdown-body h4 code, 470 | .markdown-body h5 tt, 471 | .markdown-body h5 code, 472 | .markdown-body h6 tt, 473 | .markdown-body h6 code { 474 | padding: 0 .2em; 475 | font-size: inherit; 476 | } 477 | 478 | .markdown-body summary h1, 479 | .markdown-body summary h2, 480 | .markdown-body summary h3, 481 | .markdown-body summary h4, 482 | .markdown-body summary h5, 483 | .markdown-body summary h6 { 484 | display: inline-block; 485 | } 486 | 487 | .markdown-body summary h1 .anchor, 488 | .markdown-body summary h2 .anchor, 489 | .markdown-body summary h3 .anchor, 490 | .markdown-body summary h4 .anchor, 491 | .markdown-body summary h5 .anchor, 492 | .markdown-body summary h6 .anchor { 493 | margin-left: -40px; 494 | } 495 | 496 | .markdown-body summary h1, 497 | .markdown-body summary h2 { 498 | padding-bottom: 0; 499 | border-bottom: 0; 500 | } 501 | 502 | .markdown-body ul.no-list, 503 | .markdown-body ol.no-list { 504 | padding: 0; 505 | list-style-type: none; 506 | } 507 | 508 | .markdown-body ol[type=a] { 509 | list-style-type: lower-alpha; 510 | } 511 | 512 | .markdown-body ol[type=A] { 513 | list-style-type: upper-alpha; 514 | } 515 | 516 | .markdown-body ol[type=i] { 517 | list-style-type: lower-roman; 518 | } 519 | 520 | .markdown-body ol[type=I] { 521 | list-style-type: upper-roman; 522 | } 523 | 524 | .markdown-body ol[type="1"] { 525 | list-style-type: decimal; 526 | } 527 | 528 | .markdown-body div>ol:not([type]) { 529 | list-style-type: decimal; 530 | } 531 | 532 | .markdown-body ul ul, 533 | .markdown-body ul ol, 534 | .markdown-body ol ol, 535 | .markdown-body ol ul { 536 | margin-top: 0; 537 | margin-bottom: 0; 538 | } 539 | 540 | .markdown-body li>p { 541 | margin-top: 16px; 542 | } 543 | 544 | .markdown-body li+li { 545 | margin-top: .25em; 546 | } 547 | 548 | .markdown-body dl { 549 | padding: 0; 550 | } 551 | 552 | .markdown-body dl dt { 553 | padding: 0; 554 | margin-top: 16px; 555 | font-size: 1em; 556 | font-style: italic; 557 | font-weight: 600; 558 | } 559 | 560 | .markdown-body dl dd { 561 | padding: 0 16px; 562 | margin-bottom: 16px; 563 | } 564 | 565 | .markdown-body table th { 566 | font-weight: 600; 567 | } 568 | 569 | .markdown-body table th, 570 | .markdown-body table td { 571 | padding: 6px 13px; 572 | border: 1px solid #30363d; 573 | } 574 | 575 | .markdown-body table tr { 576 | border-top: 1px solid #21262d; 577 | } 578 | 579 | .markdown-body table tr:nth-child(2n) { 580 | background-color: #161b22; 581 | } 582 | 583 | .markdown-body table img { 584 | background-color: transparent; 585 | } 586 | 587 | .markdown-body img[align=right] { 588 | padding-left: 20px; 589 | } 590 | 591 | .markdown-body img[align=left] { 592 | padding-right: 20px; 593 | } 594 | 595 | .markdown-body .emoji { 596 | max-width: none; 597 | vertical-align: text-top; 598 | background-color: transparent; 599 | } 600 | 601 | .markdown-body span.frame { 602 | display: block; 603 | overflow: hidden; 604 | } 605 | 606 | .markdown-body span.frame>span { 607 | display: block; 608 | float: left; 609 | width: auto; 610 | padding: 7px; 611 | margin: 13px 0 0; 612 | overflow: hidden; 613 | border: 1px solid #30363d; 614 | } 615 | 616 | .markdown-body span.frame span img { 617 | display: block; 618 | float: left; 619 | } 620 | 621 | .markdown-body span.frame span span { 622 | display: block; 623 | padding: 5px 0 0; 624 | clear: both; 625 | color: #c9d1d9; 626 | } 627 | 628 | .markdown-body span.align-center { 629 | display: block; 630 | overflow: hidden; 631 | clear: both; 632 | } 633 | 634 | .markdown-body span.align-center>span { 635 | display: block; 636 | margin: 13px auto 0; 637 | overflow: hidden; 638 | text-align: center; 639 | } 640 | 641 | .markdown-body span.align-center span img { 642 | margin: 0 auto; 643 | text-align: center; 644 | } 645 | 646 | .markdown-body span.align-right { 647 | display: block; 648 | overflow: hidden; 649 | clear: both; 650 | } 651 | 652 | .markdown-body span.align-right>span { 653 | display: block; 654 | margin: 13px 0 0; 655 | overflow: hidden; 656 | text-align: right; 657 | } 658 | 659 | .markdown-body span.align-right span img { 660 | margin: 0; 661 | text-align: right; 662 | } 663 | 664 | .markdown-body span.float-left { 665 | display: block; 666 | float: left; 667 | margin-right: 13px; 668 | overflow: hidden; 669 | } 670 | 671 | .markdown-body span.float-left span { 672 | margin: 13px 0 0; 673 | } 674 | 675 | .markdown-body span.float-right { 676 | display: block; 677 | float: right; 678 | margin-left: 13px; 679 | overflow: hidden; 680 | } 681 | 682 | .markdown-body span.float-right>span { 683 | display: block; 684 | margin: 13px auto 0; 685 | overflow: hidden; 686 | text-align: right; 687 | } 688 | 689 | .markdown-body code, 690 | .markdown-body tt { 691 | padding: .2em .4em; 692 | margin: 0; 693 | font-size: 85%; 694 | white-space: break-spaces; 695 | background-color: rgba(110, 118, 129, 0.4); 696 | border-radius: 6px; 697 | } 698 | 699 | .markdown-body code br, 700 | .markdown-body tt br { 701 | display: none; 702 | } 703 | 704 | .markdown-body del code { 705 | text-decoration: inherit; 706 | } 707 | 708 | .markdown-body samp { 709 | font-size: 85%; 710 | } 711 | 712 | .markdown-body pre code { 713 | font-size: 100%; 714 | } 715 | 716 | .markdown-body pre>code { 717 | padding: 0; 718 | margin: 0; 719 | word-break: normal; 720 | white-space: pre; 721 | background: transparent; 722 | border: 0; 723 | } 724 | 725 | .markdown-body .highlight { 726 | margin-bottom: 16px; 727 | } 728 | 729 | .markdown-body .highlight pre { 730 | margin-bottom: 0; 731 | word-break: normal; 732 | } 733 | 734 | .markdown-body .highlight pre, 735 | .markdown-body pre { 736 | padding: 16px; 737 | overflow: auto; 738 | font-size: 85%; 739 | line-height: 1.45; 740 | background-color: #161b22; 741 | border-radius: 6px; 742 | } 743 | 744 | .markdown-body pre code, 745 | .markdown-body pre tt { 746 | display: inline; 747 | max-width: auto; 748 | padding: 0; 749 | margin: 0; 750 | overflow: visible; 751 | line-height: inherit; 752 | word-wrap: normal; 753 | background-color: transparent; 754 | border: 0; 755 | } 756 | 757 | .markdown-body .csv-data td, 758 | .markdown-body .csv-data th { 759 | padding: 5px; 760 | overflow: hidden; 761 | font-size: 12px; 762 | line-height: 1; 763 | text-align: left; 764 | white-space: nowrap; 765 | } 766 | 767 | .markdown-body .csv-data .blob-num { 768 | padding: 10px 8px 9px; 769 | text-align: right; 770 | border: 0; 771 | } 772 | 773 | .markdown-body .csv-data tr { 774 | border-top: 0; 775 | } 776 | 777 | .markdown-body .csv-data th { 778 | font-weight: 600; 779 | background: #161b22; 780 | border-top: 0; 781 | } 782 | 783 | .markdown-body [data-footnote-ref]::before { 784 | content: "["; 785 | } 786 | 787 | .markdown-body [data-footnote-ref]::after { 788 | content: "]"; 789 | } 790 | 791 | .markdown-body .footnotes { 792 | font-size: 12px; 793 | color: #8b949e; 794 | border-top: 1px solid #30363d; 795 | } 796 | 797 | .markdown-body .footnotes ol { 798 | padding-left: 16px; 799 | } 800 | 801 | .markdown-body .footnotes ol ul { 802 | display: inline-block; 803 | padding-left: 16px; 804 | margin-top: 16px; 805 | } 806 | 807 | .markdown-body .footnotes li { 808 | position: relative; 809 | } 810 | 811 | .markdown-body .footnotes li:target::before { 812 | position: absolute; 813 | top: -8px; 814 | right: -8px; 815 | bottom: -8px; 816 | left: -24px; 817 | pointer-events: none; 818 | content: ""; 819 | border: 2px solid #1f6feb; 820 | border-radius: 6px; 821 | } 822 | 823 | .markdown-body .footnotes li:target { 824 | color: #c9d1d9; 825 | } 826 | 827 | .markdown-body .footnotes .data-footnote-backref g-emoji { 828 | font-family: monospace; 829 | } 830 | 831 | .markdown-body .pl-c { 832 | color: #8b949e; 833 | } 834 | 835 | .markdown-body .pl-c1, 836 | .markdown-body .pl-s .pl-v { 837 | color: #79c0ff; 838 | } 839 | 840 | .markdown-body .pl-e, 841 | .markdown-body .pl-en { 842 | color: #d2a8ff; 843 | } 844 | 845 | .markdown-body .pl-smi, 846 | .markdown-body .pl-s .pl-s1 { 847 | color: #c9d1d9; 848 | } 849 | 850 | .markdown-body .pl-ent { 851 | color: #7ee787; 852 | } 853 | 854 | .markdown-body .pl-k { 855 | color: #ff7b72; 856 | } 857 | 858 | .markdown-body .pl-s, 859 | .markdown-body .pl-pds, 860 | .markdown-body .pl-s .pl-pse .pl-s1, 861 | .markdown-body .pl-sr, 862 | .markdown-body .pl-sr .pl-cce, 863 | .markdown-body .pl-sr .pl-sre, 864 | .markdown-body .pl-sr .pl-sra { 865 | color: #a5d6ff; 866 | } 867 | 868 | .markdown-body .pl-v, 869 | .markdown-body .pl-smw { 870 | color: #ffa657; 871 | } 872 | 873 | .markdown-body .pl-bu { 874 | color: #f85149; 875 | } 876 | 877 | .markdown-body .pl-ii { 878 | color: #f0f6fc; 879 | background-color: #8e1519; 880 | } 881 | 882 | .markdown-body .pl-c2 { 883 | color: #f0f6fc; 884 | background-color: #b62324; 885 | } 886 | 887 | .markdown-body .pl-sr .pl-cce { 888 | font-weight: bold; 889 | color: #7ee787; 890 | } 891 | 892 | .markdown-body .pl-ml { 893 | color: #f2cc60; 894 | } 895 | 896 | .markdown-body .pl-mh, 897 | .markdown-body .pl-mh .pl-en, 898 | .markdown-body .pl-ms { 899 | font-weight: bold; 900 | color: #1f6feb; 901 | } 902 | 903 | .markdown-body .pl-mi { 904 | font-style: italic; 905 | color: #c9d1d9; 906 | } 907 | 908 | .markdown-body .pl-mb { 909 | font-weight: bold; 910 | color: #c9d1d9; 911 | } 912 | 913 | .markdown-body .pl-md { 914 | color: #ffdcd7; 915 | background-color: #67060c; 916 | } 917 | 918 | .markdown-body .pl-mi1 { 919 | color: #aff5b4; 920 | background-color: #033a16; 921 | } 922 | 923 | .markdown-body .pl-mc { 924 | color: #ffdfb6; 925 | background-color: #5a1e02; 926 | } 927 | 928 | .markdown-body .pl-mi2 { 929 | color: #c9d1d9; 930 | background-color: #1158c7; 931 | } 932 | 933 | .markdown-body .pl-mdr { 934 | font-weight: bold; 935 | color: #d2a8ff; 936 | } 937 | 938 | .markdown-body .pl-ba { 939 | color: #8b949e; 940 | } 941 | 942 | .markdown-body .pl-sg { 943 | color: #484f58; 944 | } 945 | 946 | .markdown-body .pl-corl { 947 | text-decoration: underline; 948 | color: #a5d6ff; 949 | } 950 | 951 | .markdown-body g-emoji { 952 | display: inline-block; 953 | min-width: 1ch; 954 | font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 955 | font-size: 1em; 956 | font-style: normal !important; 957 | font-weight: 400; 958 | line-height: 1; 959 | vertical-align: -0.075em; 960 | } 961 | 962 | .markdown-body g-emoji img { 963 | width: 1em; 964 | height: 1em; 965 | } 966 | 967 | .markdown-body .task-list-item { 968 | list-style-type: none; 969 | } 970 | 971 | .markdown-body .task-list-item label { 972 | font-weight: 400; 973 | } 974 | 975 | .markdown-body .task-list-item.enabled label { 976 | cursor: pointer; 977 | } 978 | 979 | .markdown-body .task-list-item+.task-list-item { 980 | margin-top: 4px; 981 | } 982 | 983 | .markdown-body .task-list-item .handle { 984 | display: none; 985 | } 986 | 987 | .markdown-body .task-list-item-checkbox { 988 | margin: 0 .2em .25em -1.4em; 989 | vertical-align: middle; 990 | } 991 | 992 | .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { 993 | margin: 0 -1.6em .25em .2em; 994 | } 995 | 996 | .markdown-body .contains-task-list { 997 | position: relative; 998 | } 999 | 1000 | .markdown-body .contains-task-list:hover .task-list-item-convert-container, 1001 | .markdown-body .contains-task-list:focus-within .task-list-item-convert-container { 1002 | display: block; 1003 | width: auto; 1004 | height: 24px; 1005 | overflow: visible; 1006 | clip: auto; 1007 | } 1008 | 1009 | .markdown-body ::-webkit-calendar-picker-indicator { 1010 | filter: invert(50%); 1011 | } 1012 | 1013 | .markdown-body pre { 1014 | position: relative; 1015 | } 1016 | 1017 | .copy { 1018 | width: 24px; 1019 | height: 24px; 1020 | display: flex; 1021 | align-items: center; 1022 | justify-content: center; 1023 | position: absolute; 1024 | right: 5px; 1025 | top: 5px; 1026 | border-radius: 4px; 1027 | background-color: #21262d; 1028 | border: 1px solid rgba(240, 246, 252, 0.1); 1029 | cursor: pointer; 1030 | } 1031 | 1032 | .copy:hover { 1033 | background-color: #30363d; 1034 | border-color: #8b949e; 1035 | } 1036 | 1037 | .copy svg { 1038 | width: 14px; 1039 | height: 14px; 1040 | stroke: #8b949e; 1041 | fill: #8b949e; 1042 | } 1043 | 1044 | .copied { 1045 | border-color: var(--success-color); 1046 | } 1047 | 1048 | .copied:hover { 1049 | border-color: var(--success-color); 1050 | } 1051 | 1052 | .copied svg { 1053 | stroke: var(--success-color); 1054 | fill: var(--success-color); 1055 | } 1056 | 1057 | .blink { 1058 | animation-name: blink; 1059 | animation-duration: 1s; 1060 | animation-iteration-count: infinite; 1061 | } 1062 | 1063 | @keyframes blink { 1064 | 0% { 1065 | opacity: 0; 1066 | } 1067 | 1068 | 50% { 1069 | opacity: 1; 1070 | } 1071 | 1072 | 100% { 1073 | opacity: 0; 1074 | } 1075 | } 1076 | --------------------------------------------------------------------------------