├── env.d.ts ├── public ├── logo.ico ├── logo.png ├── config.ts └── configModel.ts ├── src ├── assets │ ├── logo.png │ ├── css │ │ ├── global-dark.css │ │ └── global-light.css │ ├── logo.svg │ ├── main.css │ └── base.css ├── components │ ├── ControlCenter │ │ ├── HomeView.vue │ │ ├── AboutView.vue │ │ ├── NotesView.vue │ │ ├── SettingsView.vue │ │ └── PromptsView.vue │ └── HelloWorld.vue ├── views │ ├── PromptView.vue │ ├── HomeView.vue │ └── ControlCenterView.vue ├── stores │ └── counter.ts ├── main.ts ├── App.vue ├── request │ └── network.ts ├── renderer.d.ts ├── router │ └── index.ts ├── components.d.ts └── auto-imports.d.ts ├── tsconfig.config.json ├── tsconfig.json ├── index.html ├── .gitignore ├── .eslintrc.cjs ├── LICENSE ├── electron ├── preload.js ├── update │ ├── helperUpdater.ts │ └── autoUpdater.ts ├── prompt │ └── prompt.ts ├── main.ts └── menu.ts ├── vite.config.ts ├── README.md └── package.json /env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /public/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karenina-na/Claude-Desktop/HEAD/public/logo.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karenina-na/Claude-Desktop/HEAD/public/logo.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karenina-na/Claude-Desktop/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/ControlCenter/HomeView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/ControlCenter/AboutView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/ControlCenter/NotesView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/views/PromptView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/css/global-dark.css: -------------------------------------------------------------------------------- 1 | /*暗黑色调*/ 2 | html.dark { 3 | transition: all 0.2s; 4 | background-color: #1d1e1f; 5 | color: #fff; 6 | --el-transition-duration: 0.2s; 7 | --el-transition-duration-fast: 0.2s; 8 | --el-transition-all: 0.2s; 9 | } -------------------------------------------------------------------------------- /src/assets/css/global-light.css: -------------------------------------------------------------------------------- 1 | /*明亮色调*/ 2 | html.light { 3 | transition: all 0.2s; 4 | background-color: #fff; 5 | color: #000; 6 | --el-transition-duration: 0.2s; 7 | --el-transition-duration-fast: 0.2s; 8 | --el-transition-all: 0.2s; 9 | } -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | }, 10 | 11 | "references": [ 12 | { 13 | "path": "./tsconfig.config.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/stores/counter.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useCounterStore = defineStore('counter', () => { 5 | const count = ref(0) 6 | const doubleCount = computed(() => count.value * 2) 7 | function increment() { 8 | count.value++ 9 | } 10 | 11 | return { count, doubleCount, increment } 12 | }) 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.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 | .DS_Store 12 | dist 13 | dist-electron/* 14 | dist-client/* 15 | dist-ssr 16 | coverage 17 | *.local 18 | 19 | /cypress/videos/ 20 | /cypress/screenshots/ 21 | 22 | # Editor directories and files 23 | .vscode/* 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | # config 32 | config/* -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 'latest' 13 | }, 14 | "globals": { 15 | "Atomics": "readonly", 16 | "SharedArrayBuffer": "readonly", 17 | "process": true 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import 'element-plus/dist/index.css' //element-plus css 4 | import 'element-plus/theme-chalk/dark/css-vars.css' //element-plus dark theme 5 | // import './assets/css/global-light.css' 6 | // import './assets/css/global-dark.css' 7 | import router from './router' 8 | import axios from "axios"; 9 | import App from './App.vue' 10 | 11 | const app = createApp(App) 12 | 13 | app.use(createPinia()) 14 | app.use(router) 15 | app.config.globalProperties.$axios = axios 16 | 17 | app.mount('#app') 18 | 19 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 35 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/request/network.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const service = axios.create({ 4 | baseURL: (process.env.VITE_NODE_MODE === 'production' ? process.env.VITE_THEMISE_URL + "" : "/api") + "/v1", 5 | timeout: 5000 6 | }); 7 | 8 | // Request interceptors 9 | service.interceptors.request.use((config) => { 10 | const headers = new Headers(); 11 | headers.append("Content-Type", "application/json"); 12 | return config; 13 | }, error => { 14 | return Promise.reject(error); 15 | }); 16 | 17 | // Response interceptors 18 | service.interceptors.response.use(response => { 19 | return response.data; 20 | }, error => { 21 | return Promise.reject(error); 22 | }); 23 | 24 | export default service; -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 41 | -------------------------------------------------------------------------------- /src/renderer.d.ts: -------------------------------------------------------------------------------- 1 | // if you need to add some types for electron, you can add here 2 | // for electron ipcRenderer 3 | export interface IElectronAPI { 4 | quit: () => void, 5 | getConfigPath: () => Promise, 6 | openConfig: () => void, 7 | getUpdateInfo: () => Promise, 8 | setUpdateInfo: (arg: any) => Promise, 9 | resetUpdateInfo: () => Promise, 10 | getConfig: () => Promise, 11 | updateConfig: (config: any) => Promise, 12 | resetConfig: () => Promise, 13 | getPromptURL: () => Promise, 14 | openPrompt() : Promise, 15 | getPrompt: () => Promise, 16 | setPrompt: (prompt: any) => Promise, 17 | resetPrompt: () => Promise, 18 | syncPrompt: () => Promise, 19 | } 20 | 21 | declare global { 22 | interface Window { 23 | electronAPI: IElectronAPI 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Wei Zixiang 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/preload.js: -------------------------------------------------------------------------------- 1 | // load node.js modules in the renderer process 2 | window.addEventListener('DOMContentLoaded', () => { 3 | // console.log('DOMContentLoaded') 4 | 5 | const { contextBridge, ipcRenderer } = require('electron') 6 | 7 | contextBridge.exposeInMainWorld('electronAPI', { 8 | getUpdateInfo: () => ipcRenderer.invoke('getUpdateInfo'), 9 | setUpdateInfo: (updateInfo) => ipcRenderer.invoke('setUpdateInfo', updateInfo), 10 | resetUpdateInfo: () => ipcRenderer.invoke('resetUpdateInfo'), 11 | quit: () => ipcRenderer.send('quit'), 12 | getConfigPath: () => ipcRenderer.invoke('getConfigPath'), 13 | getConfig: () => ipcRenderer.invoke('getConfig'), 14 | openConfig: () => ipcRenderer.send('openConfig'), 15 | updateConfig: (config) => ipcRenderer.invoke('updateConfig', config), 16 | resetConfig: () => ipcRenderer.invoke('resetConfig'), 17 | getPromptURL: () => ipcRenderer.invoke('getPromptURL'), 18 | openPrompt: () => ipcRenderer.send('openPrompt'), 19 | getPrompt: () => ipcRenderer.invoke('getPrompt'), 20 | setPrompt: (prompt) => ipcRenderer.invoke('setPrompt', prompt), 21 | resetPrompt: () => ipcRenderer.invoke('resetPrompt'), 22 | syncPrompt: () => ipcRenderer.invoke('syncPrompt'), 23 | }) 24 | }) -------------------------------------------------------------------------------- /electron/update/helperUpdater.ts: -------------------------------------------------------------------------------- 1 | // help for auto update 2 | 3 | import { join } from 'path' 4 | import fs from 'fs' 5 | import { app } from 'electron' 6 | const dataPath = join(join(app.getPath('home'), '.claude'), 'updateInfo.json') 7 | 8 | function getLocalData(key?:any) { 9 | if (!fs.existsSync(dataPath)) { 10 | fs.writeFileSync(dataPath, JSON.stringify({}), { encoding: 'utf-8' }) 11 | } 12 | let data = fs.readFileSync(dataPath, { encoding: 'utf-8' }) 13 | let json = JSON.parse(data) 14 | return key ? json[key] : json 15 | } 16 | 17 | function setLocalData(key?:any, value?:any,) { 18 | let args = [...arguments] 19 | let data = fs.readFileSync(dataPath, { encoding: 'utf-8' }) 20 | let json = JSON.parse(data) 21 | if (args.length === 0 || args[0] === null) { 22 | json = {} 23 | } else if (args.length === 1 && typeof key === 'object' && key) { 24 | json = { 25 | ...json, 26 | ...args[0], 27 | } 28 | } else { 29 | json[key] = value 30 | } 31 | fs.writeFileSync(dataPath, JSON.stringify(json), { encoding: 'utf-8' }) 32 | } 33 | 34 | async function sleep(ms:any) { 35 | return new Promise((resolve:any) => { 36 | const timer = setTimeout(() => { 37 | resolve() 38 | clearTimeout(timer) 39 | }, ms) 40 | }) 41 | } 42 | 43 | export { 44 | getLocalData, 45 | setLocalData, 46 | sleep, 47 | dataPath 48 | } 49 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHashHistory} from 'vue-router' 2 | 3 | const router = createRouter({ 4 | history: createWebHashHistory(import.meta.env.BASE_URL), 5 | routes: [{ 6 | path: '/', 7 | name: 'home', 8 | component: () => import('@/views/HomeView.vue') 9 | }, { 10 | path: '/controlCenter', 11 | name: 'controlCenter', 12 | component: () => import('@/views/ControlCenterView.vue'), 13 | children: [{ 14 | path: '/controlCenter', 15 | name: 'controlCenterHome', 16 | component: () => import('@/components/ControlCenter/HomeView.vue') 17 | },{ 18 | path: '/controlCenter/settings', 19 | name: 'settings', 20 | component: () => import('@/components/ControlCenter/SettingsView.vue') 21 | },{ 22 | path: '/controlCenter/prompts', 23 | name: 'prompts', 24 | component: () => import('@/components/ControlCenter/PromptsView.vue') 25 | },{ 26 | path: '/controlCenter/notes', 27 | name: 'notes', 28 | component: () => import('@/components/ControlCenter/NotesView.vue') 29 | },{ 30 | path: '/controlCenter/about', 31 | name: 'about', 32 | component: () => import('@/components/ControlCenter/AboutView.vue') 33 | }] 34 | }, { 35 | path: '/prompt', 36 | name: 'prompt', 37 | component: () => import('@/views/PromptView.vue') 38 | }] 39 | }) 40 | 41 | export default router 42 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import electron from 'vite-plugin-electron' 6 | import Inspect from 'vite-plugin-inspect' 7 | 8 | import Icons from 'unplugin-icons/vite' 9 | import IconsResolver from 'unplugin-icons/resolver' 10 | 11 | import AutoImport from 'unplugin-auto-import/vite' 12 | import Components from 'unplugin-vue-components/vite' 13 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 14 | 15 | const pathSrc = path.resolve(__dirname, 'src') 16 | 17 | // https://vitejs.dev/config/ 18 | export default defineConfig({ 19 | base: "./", 20 | resolve: { 21 | alias: { 22 | '@': pathSrc 23 | } 24 | }, 25 | plugins: [ 26 | vue(), 27 | electron({ 28 | entry: 'electron/main.ts', 29 | }, 30 | ), 31 | AutoImport({ 32 | imports: ['vue'], 33 | resolvers: [ 34 | ElementPlusResolver(), 35 | IconsResolver({ 36 | prefix: 'Icon', 37 | }), 38 | ], 39 | dts: path.resolve(pathSrc, 'auto-imports.d.ts'), 40 | }), 41 | Components({ 42 | resolvers: [ 43 | IconsResolver({ 44 | enabledCollections: ['ep'], 45 | }), 46 | ElementPlusResolver(), 47 | ], 48 | dts: path.resolve(pathSrc, 'components.d.ts'), 49 | }), 50 | Icons({ 51 | autoInstall: true, 52 | }), 53 | Inspect(), 54 | ], 55 | }) 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Claude 3 |

Claude

4 |

Claude Desktop App (Windows)

5 |
6 | 7 | --- 8 | 9 | **Claude to Desktop is an Electron-based desktop application for Claude2.** 10 | 11 | ✨ **It is an unofficial project, for personal learning and research purposes.** 12 | 13 | ⚗️ Claude2 is a powerful cloud-based note-taking application, and this project aims to bring its functionality to the desktop environment. 14 | 15 | *** 16 | 17 | ## 📦 To Use 18 | 19 | To clone and run this repository you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line: 20 | 21 | ```bash 22 | # Clone this repository 23 | git clone https://github.com/Karenina-na/Claude-Desktop.git 24 | # Go into the repository 25 | cd Claude-Desktop 26 | # Install dependencies 27 | npm install 28 | # Run the app 29 | npm run dev 30 | ``` 31 | 32 | ## 📢 Announcement 33 | 34 | I am a novice programmer, brimming with passion for coding, constantly engaged in learning and advancing. This project is the fruit of my learning journey, albeit possibly containing some imperfections. However, I shall persistently refine it and strive to elevate its excellence. 35 | 36 | I wholeheartedly welcome everyone's suggestions, opinions, and critiques concerning this project. Should you have any queries or ideas, feel free to engage in a meaningful exchange with me. Together, we shall progress, learn in unison, and mutually inspire one another!" 37 | 38 | ## 🤝 contribute 39 | 40 | 1. Contribute to this endeavor, `Fork` the present undertaking. 41 | 2. Establish your distinctive branch of characteristics. (`git checkout -b feature/AmazingFeature`) 42 | 3. Submit your modifications forthwith. (`git commit -m 'Add some AmazingFeature'`) 43 | 4. Propagate your branch to the remote repository with due diligence. (`git push origin feature/AmazingFeature`) 44 | 5. Submit a formal pull request for consideration. 45 | 46 | ## License 47 | 48 | [MIT LICENSE](LICENSE) 49 | 50 | 51 | ## 📞 Contact Information 52 | 53 | Should you have any questions or concerns regarding the project, please feel free to contact me via the following methods: 54 | 55 | - Email: weizixiang0@outlook.com 56 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /public/config.ts: -------------------------------------------------------------------------------- 1 | // create config file 2 | import path from "path"; 3 | import { app} from "electron"; 4 | 5 | import fs from "fs"; 6 | import { dialog } from "electron"; 7 | import configModel from "./configModel"; 8 | import logger from "electron-log"; 9 | 10 | 11 | // create and load config file 12 | function ConfigFactory(){ 13 | let config: configModel; 14 | 15 | // config dir 16 | const configDir = path.join(app.getPath('home'), '.claude'); 17 | // config file 18 | const configFile = path.join(configDir, 'config.json'); 19 | 20 | // error dialog 21 | const error = (message:string) => { 22 | dialog.showErrorBox('Error', message) 23 | logger.error(message) 24 | } 25 | 26 | try{ 27 | fs.statSync(configDir) 28 | // config dir exists 29 | try{ 30 | fs.statSync(configFile) 31 | // config file exists 32 | config = new configModel() 33 | Object.assign(config, JSON.parse(fs.readFileSync(configFile).toString())) 34 | }catch(err){ 35 | // config file not exists 36 | config = new configModel() 37 | fs.writeFileSync(configFile, JSON.stringify(config, null, 1)) 38 | } 39 | }catch(err){ 40 | // config dir not exists 41 | try{ 42 | fs.mkdirSync(configDir) 43 | // mkdir success 44 | config = new configModel() 45 | fs.writeFileSync(configFile, JSON.stringify(config, null, 1)) 46 | }catch(err){ 47 | 48 | // mkdir failed 49 | error(err.message) 50 | } 51 | } 52 | 53 | return config; 54 | } 55 | 56 | // load and update config file 57 | function ConfigUpdate(config: configModel){ 58 | // config dir 59 | const configDir = path.join(app.getPath('home'), '.claude'); 60 | // config file 61 | const configFile = path.join(configDir, 'config.json'); 62 | 63 | // error dialog 64 | const error = (message:string) => { 65 | dialog.showErrorBox('Error', message) 66 | logger.error(message) 67 | } 68 | 69 | try{ 70 | fs.statSync(configDir) 71 | // config dir exists 72 | try{ 73 | fs.statSync(configFile) 74 | // config file exists 75 | fs.writeFileSync(configFile, JSON.stringify(config, null, 1)) 76 | }catch(err){ 77 | // config file not exists 78 | error(err.message) 79 | } 80 | }catch(err){ 81 | // config dir not exists 82 | error(err.message) 83 | } 84 | } 85 | 86 | export {ConfigFactory, ConfigUpdate} -------------------------------------------------------------------------------- /src/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AboutView: typeof import('./components/ControlCenter/AboutView.vue')['default'] 11 | ElAside: typeof import('element-plus/es')['ElAside'] 12 | ElButton: typeof import('element-plus/es')['ElButton'] 13 | ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] 14 | ElContainer: typeof import('element-plus/es')['ElContainer'] 15 | ElIcon: typeof import('element-plus/es')['ElIcon'] 16 | ElInput: typeof import('element-plus/es')['ElInput'] 17 | ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] 18 | ElMain: typeof import('element-plus/es')['ElMain'] 19 | ElPagination: typeof import('element-plus/es')['ElPagination'] 20 | ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm'] 21 | ElRadio: typeof import('element-plus/es')['ElRadio'] 22 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 23 | ElRate: typeof import('element-plus/es')['ElRate'] 24 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] 25 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 26 | ElTable: typeof import('element-plus/es')['ElTable'] 27 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 28 | ElTabPane: typeof import('element-plus/es')['ElTabPane'] 29 | ElTabs: typeof import('element-plus/es')['ElTabs'] 30 | ElTooltip: typeof import('element-plus/es')['ElTooltip'] 31 | HelloWorld: typeof import('./components/HelloWorld.vue')['default'] 32 | HomeView: typeof import('./components/ControlCenter/HomeView.vue')['default'] 33 | IEpCheck: typeof import('~icons/ep/check')['default'] 34 | IEpClose: typeof import('~icons/ep/close')['default'] 35 | IEpDArrowLeft: typeof import('~icons/ep/d-arrow-left')['default'] 36 | IEpDArrowRight: typeof import('~icons/ep/d-arrow-right')['default'] 37 | IEpEdit: typeof import('~icons/ep/edit')['default'] 38 | IEpMagicStick: typeof import('~icons/ep/magic-stick')['default'] 39 | IEpSetting: typeof import('~icons/ep/setting')['default'] 40 | IEpWarning: typeof import('~icons/ep/warning')['default'] 41 | NotesView: typeof import('./components/ControlCenter/NotesView.vue')['default'] 42 | PromptsView: typeof import('./components/ControlCenter/PromptsView.vue')['default'] 43 | RouterLink: typeof import('vue-router')['RouterLink'] 44 | RouterView: typeof import('vue-router')['RouterView'] 45 | SettingsView: typeof import('./components/ControlCenter/SettingsView.vue')['default'] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claude-desktop", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": { 6 | "name": "Karenina-na", 7 | "email": "weizixiang0@outlook.com" 8 | }, 9 | "description": "An Claude desktop application built with Vite + Vue 3.x + Electron 13.x", 10 | "main": "dist-electron/main.js", 11 | "scripts": { 12 | "dev": "vite", 13 | "build": "vue-tsc --noEmit && vite build && electron-builder", 14 | "preview": "vite preview --port 4173", 15 | "build-only": "vite build", 16 | "type-check": "vue-tsc --noEmit", 17 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" 18 | }, 19 | "dependencies": { 20 | "@element-plus/icons-vue": "^2.1.0", 21 | "@vueuse/core": "^10.3.0", 22 | "axios": "^1.4.0", 23 | "electron-log": "^4.4.8", 24 | "electron-updater": "^6.1.1", 25 | "element-plus": "^2.3.8", 26 | "perfect-scrollbar": "^1.5.5", 27 | "pinia": "^2.0.21", 28 | "vite-plugin-inspect": "^0.7.35", 29 | "vue": "^3.2.38", 30 | "vue-router": "^4.1.5" 31 | }, 32 | "devDependencies": { 33 | "@iconify-json/ep": "^1.1.11", 34 | "@rushstack/eslint-patch": "^1.1.4", 35 | "@types/node": "^20.4.2", 36 | "@vitejs/plugin-vue": "^3.0.3", 37 | "@vue/eslint-config-typescript": "^11.0.0", 38 | "@vue/tsconfig": "^0.4.0", 39 | "electron": "^25.3.1", 40 | "electron-builder": "^24.4.0", 41 | "eslint": "^8.22.0", 42 | "eslint-plugin-vue": "^9.3.0", 43 | "npm-run-all": "^4.1.5", 44 | "typescript": "~5.1.6", 45 | "unplugin-auto-import": "^0.16.6", 46 | "unplugin-icons": "^0.16.5", 47 | "unplugin-vue-components": "^0.25.1", 48 | "vite": "^3.0.9", 49 | "vite-plugin-electron": "^0.12.0", 50 | "vue-tsc": "^1.8.5" 51 | }, 52 | "build": { 53 | "appId": "Karenina-na.Claude", 54 | "artifactName": "${productName}-setup-${version}.${ext}", 55 | "productName": "Claude-Desktop", 56 | "copyright": "Copyright © 2023 Karenina-na", 57 | "directories": { 58 | "output": "dist-client" 59 | }, 60 | "publish": [ 61 | { 62 | "provider": "github", 63 | "owner": "Karenina-na", 64 | "repo": "Claude-Desktop", 65 | "releaseType": "release", 66 | "protocol": "https" 67 | } 68 | ], 69 | "win": { 70 | "icon": "./public/logo.ico", 71 | "target": [ 72 | { 73 | "target": "nsis", 74 | "arch": [ 75 | "x64" 76 | ] 77 | } 78 | ] 79 | }, 80 | "nsis": { 81 | "oneClick": "false", 82 | "perMachine": "true", 83 | "allowToChangeInstallationDirectory": "true", 84 | "installerIcon": "./public/logo.ico", 85 | "uninstallerIcon": "./public/logo.ico", 86 | "installerHeaderIcon": "./public/logo.ico" 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /public/configModel.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import {app} from "electron"; 3 | import fs from "fs"; 4 | 5 | class Config{ 6 | private _theme: string; 7 | private _stay_on_top: boolean; 8 | private _main_width: number; 9 | private _main_height: number; 10 | private _tray: boolean; 11 | private _tray_width: number; 12 | private _tray_height: number; 13 | private _ua_tray: string; 14 | private _prompt_path: string; 15 | 16 | constructor(){ 17 | this._theme = "system"; 18 | this._stay_on_top = false; 19 | this._main_width = 1080; 20 | this._main_height = 768; 21 | this._tray = true; 22 | this._tray_width = 400; 23 | this._tray_height = 600; 24 | this._ua_tray = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1"; 25 | this._prompt_path = path.join(path.join(app.getPath('home'), '.claude'), 'prompt'); 26 | 27 | // create prompt dir 28 | try{ 29 | fs.statSync(path.join(app.getPath('home'), '.claude', 'prompt')) 30 | }catch(err){ 31 | fs.mkdirSync(path.join(app.getPath('home'), '.claude', 'prompt')) 32 | } 33 | } 34 | 35 | get theme(): string { 36 | return this._theme; 37 | } 38 | 39 | set theme(value: string) { 40 | this._theme = value; 41 | } 42 | 43 | get stay_on_top(): boolean { 44 | return this._stay_on_top; 45 | } 46 | 47 | set stay_on_top(value: boolean) { 48 | this._stay_on_top = value; 49 | } 50 | 51 | get main_width(): number { 52 | return this._main_width; 53 | } 54 | 55 | set main_width(value: number) { 56 | this._main_width = value; 57 | } 58 | 59 | get main_height(): number { 60 | return this._main_height; 61 | } 62 | 63 | set main_height(value: number) { 64 | this._main_height = value; 65 | } 66 | 67 | get tray(): boolean { 68 | return this._tray; 69 | } 70 | 71 | set tray(value: boolean) { 72 | this._tray = value; 73 | } 74 | 75 | get tray_width(): number { 76 | return this._tray_width; 77 | } 78 | 79 | set tray_width(value: number) { 80 | this._tray_width = value; 81 | } 82 | 83 | get tray_height(): number { 84 | return this._tray_height; 85 | } 86 | 87 | set tray_height(value: number) { 88 | this._tray_height = value; 89 | } 90 | 91 | get ua_tray(): string { 92 | return this._ua_tray; 93 | } 94 | 95 | set ua_tray(value: string) { 96 | this._ua_tray = value; 97 | } 98 | 99 | get prompt_path(): string { 100 | return this._prompt_path; 101 | } 102 | 103 | set prompt_path(value: string) { 104 | this._prompt_path = value; 105 | } 106 | 107 | } 108 | 109 | export default Config; -------------------------------------------------------------------------------- /src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const EffectScope: typeof import('vue')['EffectScope'] 9 | const ElNotification: typeof import('element-plus/es')['ElNotification'] 10 | const computed: typeof import('vue')['computed'] 11 | const createApp: typeof import('vue')['createApp'] 12 | const customRef: typeof import('vue')['customRef'] 13 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 14 | const defineComponent: typeof import('vue')['defineComponent'] 15 | const effectScope: typeof import('vue')['effectScope'] 16 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 17 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 18 | const h: typeof import('vue')['h'] 19 | const inject: typeof import('vue')['inject'] 20 | const isProxy: typeof import('vue')['isProxy'] 21 | const isReactive: typeof import('vue')['isReactive'] 22 | const isReadonly: typeof import('vue')['isReadonly'] 23 | const isRef: typeof import('vue')['isRef'] 24 | const markRaw: typeof import('vue')['markRaw'] 25 | const nextTick: typeof import('vue')['nextTick'] 26 | const onActivated: typeof import('vue')['onActivated'] 27 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 28 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 29 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 30 | const onDeactivated: typeof import('vue')['onDeactivated'] 31 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 32 | const onMounted: typeof import('vue')['onMounted'] 33 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 34 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 35 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 36 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 37 | const onUnmounted: typeof import('vue')['onUnmounted'] 38 | const onUpdated: typeof import('vue')['onUpdated'] 39 | const provide: typeof import('vue')['provide'] 40 | const reactive: typeof import('vue')['reactive'] 41 | const readonly: typeof import('vue')['readonly'] 42 | const ref: typeof import('vue')['ref'] 43 | const resolveComponent: typeof import('vue')['resolveComponent'] 44 | const shallowReactive: typeof import('vue')['shallowReactive'] 45 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 46 | const shallowRef: typeof import('vue')['shallowRef'] 47 | const toRaw: typeof import('vue')['toRaw'] 48 | const toRef: typeof import('vue')['toRef'] 49 | const toRefs: typeof import('vue')['toRefs'] 50 | const toValue: typeof import('vue')['toValue'] 51 | const triggerRef: typeof import('vue')['triggerRef'] 52 | const unref: typeof import('vue')['unref'] 53 | const useAttrs: typeof import('vue')['useAttrs'] 54 | const useCssModule: typeof import('vue')['useCssModule'] 55 | const useCssVars: typeof import('vue')['useCssVars'] 56 | const useSlots: typeof import('vue')['useSlots'] 57 | const watch: typeof import('vue')['watch'] 58 | const watchEffect: typeof import('vue')['watchEffect'] 59 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 60 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 61 | } 62 | // for type re-export 63 | declare global { 64 | // @ts-ignore 65 | export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue' 66 | } 67 | -------------------------------------------------------------------------------- /electron/update/autoUpdater.ts: -------------------------------------------------------------------------------- 1 | // src/main/autoUpdater.js 2 | 3 | import { app, dialog } from 'electron' 4 | import { autoUpdater } from 'electron-updater' 5 | import logger from 'electron-log' 6 | import { name } from '../../package.json' 7 | import { getLocalData, setLocalData, sleep } from './helperUpdater' 8 | 9 | export async function autoUpdateInit() { 10 | autoUpdater.logger = logger 11 | autoUpdater.disableWebInstaller = false 12 | autoUpdater.autoDownload = false // Please write this as "false." When written as "true," I am encountering a permission issue and unable to determine the exact cause. 13 | autoUpdater.on('error', (error) => { 14 | logger.error(['check update error', error]) 15 | }) 16 | // Upon detecting available updates, it will be triggered to automatically initiate the download process for the updates. 17 | autoUpdater.on('update-available', (info) => { 18 | logger.info('Updates have been detected. Initiating the download of the new version.') 19 | logger.info(info) 20 | const { version } = info 21 | askUpdate(version) 22 | }) 23 | // Upon detecting the absence of available updates, it will be triggered accordingly. 24 | autoUpdater.on('update-not-available', () => { 25 | logger.info('No updates have been detected.') 26 | }) 27 | // Implementing the delta download logic during the application's startup. 28 | autoUpdater.on('download-progress', async (progress) => { 29 | logger.info(progress) 30 | }) 31 | // Upon the completion of the update download process, it will be triggered accordingly. 32 | autoUpdater.on('update-downloaded', (res) => { 33 | logger.info('Download complete! Prompting to install updates.') 34 | logger.info(res) 35 | // To use 'dialog,' it must be created after the 'BrowserWindow' has been created. 36 | dialog.showMessageBox({ 37 | type: 'info', 38 | buttons: ['OK'], 39 | title: 'Upgrade notification.!', 40 | message: 'The latest application has been downloaded for you. \n' + 41 | 'Click \'OK\' to replace it with the newest version immediately!', 42 | textWidth: 250, 43 | }) 44 | .then(() => { 45 | logger.info('Exit the application, installation will begin!') 46 | // Restart the application and install the update after downloading. 47 | // It should only be called after the 'update-downloaded' event has been emitted. 48 | autoUpdater.quitAndInstall() 49 | }) 50 | }) 51 | 52 | await sleep(3000) 53 | // Each time it starts, it will automatically perform a version check and update itself accordingly, 54 | // either through scheduled updates or any other suitable method. 55 | await autoUpdater.checkForUpdates() 56 | } 57 | 58 | async function askUpdate(version:any) { 59 | logger.info(`The latest version. ${version}`) 60 | let { updater } = getLocalData() 61 | let { auto, version: ver, skip } = updater || {} 62 | logger.info( 63 | JSON.stringify({ 64 | ...updater, 65 | ver: ver, 66 | }) 67 | ) 68 | if (skip && version === ver) return 69 | if (auto) { 70 | // Proceed with the update download without further inquiry. 71 | await autoUpdater.downloadUpdate() 72 | } else { 73 | const { response, checkboxChecked } = await dialog.showMessageBox({ 74 | type: 'question', 75 | title: `${name} software update notification.`, 76 | buttons: ['Closing', 'Skipping this version.', 'Installing the update.'], 77 | message: `The latest version is ${version},the current version is ${app.getVersion()},Is it necessary to download updates now ?`, 78 | defaultId: 2, 79 | cancelId: -1, 80 | checkboxLabel: 'Automatic updates installation.', 81 | checkboxChecked: false, 82 | textWidth: 250, 83 | }) 84 | if ([1, 2].includes(response)) { 85 | let updaterData = { 86 | version: version, 87 | skip: response === 1, 88 | auto: checkboxChecked, 89 | } 90 | setLocalData({ 91 | updater: { 92 | ...updaterData, 93 | }, 94 | }) 95 | if (response === 2) await autoUpdater.downloadUpdate() 96 | logger.info(['Updating process.', JSON.stringify(updaterData)]) 97 | } else { 98 | logger.info(['Updating process.', 'Disable update notifications.']) 99 | } 100 | } 101 | } 102 | 103 | -------------------------------------------------------------------------------- /electron/prompt/prompt.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path' 2 | import fs from 'fs' 3 | import logger from 'electron-log' 4 | import https from "https"; 5 | 6 | const promptUrl = 'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv' 7 | // const promptPath = join(app.getPath('home'), '.claude', 'prompt') 8 | // const promptInfo = join(promptPath, 'promptInfo.json') 9 | 10 | 11 | function downloadAndGetPrompt(promptPath:string) { 12 | const promptInfo = join(promptPath, 'promptInfo.json') 13 | // csv 14 | if (!fs.existsSync(join(promptPath, 'prompts.csv'))) { 15 | downloadPrompt(promptPath) 16 | } 17 | 18 | let promptList = [] 19 | 20 | // make promptInfo.json 21 | if (fs.existsSync(join(promptPath, 'prompts.csv')) && !fs.existsSync(promptInfo)) { 22 | let data = fs.readFileSync(join(promptPath, 'prompts.csv'), { encoding: 'utf-8' }) 23 | let lines = data.split('\n') 24 | for (let i = 1; i < lines.length; i++) { 25 | let line = lines[i] 26 | // fix bug for csv 27 | let [act, prompt] = line.split('","') 28 | if (act == undefined || prompt == undefined) { 29 | continue 30 | } 31 | let info = { 32 | CMD: act.toLowerCase().replace(/ /g, '_').replace(/"/g, ''), 33 | ACT: act.replace(/"/g, ''), 34 | PROMPT: prompt.replace(/"/g, ''), 35 | ENABLE: true, 36 | } 37 | promptList.push(info) 38 | } 39 | fs.writeFileSync(promptInfo, JSON.stringify(promptList, null, 4)) 40 | }else if (fs.existsSync(promptInfo)) { 41 | 42 | // json exists 43 | promptList = JSON.parse(fs.readFileSync(promptInfo, { encoding: 'utf-8' })) 44 | } 45 | return promptList 46 | } 47 | 48 | function downloadPrompt(promptPath:string) { 49 | const file = fs.createWriteStream(join(promptPath, 'prompts.csv.temp')) 50 | logger.info('downloading prompts.csv to prompts.csv.temp') 51 | https.get(promptUrl, response=> { 52 | response.pipe(file) 53 | 54 | // finish 55 | file.on('finish', ()=> { 56 | file.close() 57 | logger.info('download prompts.csv.temp success') 58 | // rename 59 | fs.renameSync(join(promptPath, 'prompts.csv.temp'), join(promptPath, 'prompts.csv')) 60 | logger.info('rename prompts.csv.temp to prompts.csv success') 61 | }) 62 | 63 | // error 64 | file.on('error', err=> { 65 | logger.error(err) 66 | }) 67 | 68 | }).on('error', err=> { 69 | logger.error(err) 70 | }) 71 | } 72 | 73 | function setPromptInfo(promptPath:string,promptList:any[]) { 74 | const promptInfo = join(promptPath, 'promptInfo.json') 75 | fs.writeFileSync(promptInfo, JSON.stringify(promptList, null, 4)) 76 | } 77 | 78 | function resetPromptInfo(promptPath:string) { 79 | const promptInfo = join(promptPath, 'promptInfo.json') 80 | let promptList = [] 81 | if (fs.existsSync(join(promptPath, 'prompts.csv'))) { 82 | // update json 83 | let data = fs.readFileSync(join(promptPath, 'prompts.csv'), { encoding: 'utf-8' }) 84 | let lines = data.split('\n') 85 | for (let i = 1; i < lines.length; i++) { 86 | let line = lines[i] 87 | // fix bug for csv 88 | let [act, prompt] = line.split('","') 89 | if (act == undefined || prompt == undefined) { 90 | continue 91 | } 92 | let info = { 93 | CMD: act.toLowerCase().replace(/ /g, '_').replace(/"/g, ''), 94 | ACT: act.replace(/"/g, ''), 95 | PROMPT: prompt.replace(/"/g, ''), 96 | ENABLE: true, 97 | } 98 | promptList.push(info) 99 | } 100 | fs.writeFileSync(promptInfo, JSON.stringify(promptList, null, 4)) 101 | }else{ 102 | // download csv 103 | downloadPrompt(promptPath) 104 | } 105 | return promptList 106 | } 107 | 108 | async function syncPromptInfo(promptPath:string) { 109 | const promptInfo = join(promptPath, 'promptInfo.json') 110 | // download 111 | await downloadPrompt(promptPath) 112 | // update json 113 | let promptList = [] 114 | if (fs.existsSync(join(promptPath, 'prompts.csv'))) { 115 | // update json 116 | let data = fs.readFileSync(join(promptPath, 'prompts.csv'), { encoding: 'utf-8' }) 117 | let lines = data.split('\n') 118 | for (let i = 1; i < lines.length; i++) { 119 | let line = lines[i] 120 | // fix bug for csv 121 | let [act, prompt] = line.split('","') 122 | if (act == undefined || prompt == undefined) { 123 | continue 124 | } 125 | let info = { 126 | CMD: act.toLowerCase().replace(/ /g, '_').replace(/"/g, ''), 127 | ACT: act.replace(/"/g, ''), 128 | PROMPT: prompt.replace(/"/g, ''), 129 | ENABLE: true, 130 | } 131 | promptList.push(info) 132 | } 133 | fs.writeFileSync(promptInfo, JSON.stringify(promptList, null, 4)) 134 | } 135 | } 136 | 137 | export { 138 | promptUrl, 139 | downloadAndGetPrompt, 140 | setPromptInfo, 141 | resetPromptInfo, 142 | syncPromptInfo, 143 | } -------------------------------------------------------------------------------- /electron/main.ts: -------------------------------------------------------------------------------- 1 | import {app, BrowserWindow, ipcMain, Menu, protocol, shell, Tray} from 'electron' 2 | import menuTemplate from "./menu"; 3 | import {ConfigFactory, ConfigUpdate} from "../public/config" 4 | import path, {join} from "path"; 5 | import Config from "../public/configModel"; 6 | import {autoUpdateInit} from "./update/autoUpdater"; 7 | import {getLocalData, setLocalData} from "./update/helperUpdater"; 8 | import { promptUrl, downloadAndGetPrompt, setPromptInfo, resetPromptInfo, syncPromptInfo,} from "./prompt/prompt"; 9 | import logger from 'electron-log' 10 | 11 | // log 12 | logger.transports.file.maxSize = 1002430 // 10M 13 | logger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}'; 14 | logger.transports.file.resolvePath = () => join(path.join(app.getPath('home'), '.claude'), 'logs/claude.log') 15 | 16 | app.commandLine.appendSwitch("--ignore-certificate-errors", "true"); 17 | // Scheme must be registered before the app is ready 18 | protocol.registerSchemesAsPrivileged([ 19 | { scheme: "app", privileges: { secure: true, standard: true } } 20 | ]); 21 | 22 | // config 23 | const config = ConfigFactory(); 24 | 25 | app.whenReady().then(() => { 26 | 27 | const win= new BrowserWindow({ 28 | title: 'Claude', 29 | width: config.main_width, 30 | height: config.main_height, 31 | center: true, 32 | icon: "public/logo.png", 33 | webPreferences: { 34 | nodeIntegration: true, 35 | nodeIntegrationInWorker: true, 36 | webSecurity: false, 37 | } 38 | }) 39 | 40 | // set user agent 41 | let session = win.webContents.session; 42 | session.setUserAgent(config.ua_tray); 43 | 44 | // load url 45 | win.loadURL('https://claude.ai/chat/') 46 | 47 | // tray 48 | createTray(win); 49 | 50 | // dev tools 51 | if (!app.isPackaged) { 52 | // win.webContents.openDevTools({ mode: 'detach' }); 53 | } 54 | 55 | // stay on top 56 | if (config.stay_on_top) { 57 | if (win) { 58 | win.setAlwaysOnTop(true); 59 | } 60 | } 61 | 62 | }) 63 | 64 | // create menu 65 | app.on('ready',() =>{ 66 | const m = Menu.buildFromTemplate(menuTemplate(config)); 67 | Menu.setApplicationMenu(m); 68 | }); 69 | 70 | // create Tray 71 | function createTray(win: BrowserWindow){ 72 | // Tray 73 | const tray: Tray = new Tray(path.join(__dirname, '../public/logo.png')); 74 | let ControlCenterWin: BrowserWindow | null = null; 75 | const contextMenu = Menu.buildFromTemplate([{ 76 | label: 'Control Center', 77 | click: () => { 78 | if (ControlCenterWin) { 79 | ControlCenterWin.focus(); 80 | return; 81 | } 82 | // Create the browser window. 83 | ControlCenterWin = new BrowserWindow({ 84 | title: 'Control Center', 85 | width: 1000, 86 | height: 650, 87 | resizable: false, 88 | icon: "public/logo.png", 89 | modal: true, 90 | center: true, 91 | parent: BrowserWindow.getFocusedWindow(), 92 | webPreferences: { 93 | nodeIntegration: true, 94 | nodeIntegrationInWorker: true, 95 | webSecurity: false, 96 | preload: path.join(__dirname, '../electron/preload.js') 97 | }, 98 | }) 99 | if (app.isPackaged) { 100 | ControlCenterWin.loadFile(path.join(__dirname, '../dist/index.html'), { hash: 'controlCenter' }) 101 | } else { 102 | ControlCenterWin.loadURL('http://localhost:5173/#/controlCenter') 103 | } 104 | // Emitted when the window is closed. 105 | ControlCenterWin.on('closed', () => { 106 | ControlCenterWin = null 107 | }) 108 | } 109 | }, 110 | { label: 'Show Window', click: () => win.show() }, 111 | { label: 'Reload', click: () => win.reload() }, 112 | { type: 'separator' }, 113 | { label: 'Quit', click: () => app.quit() } 114 | ]) 115 | 116 | tray.setContextMenu(contextMenu) 117 | tray.on('click', () => { 118 | if (win.isVisible()) { 119 | win.hide(); 120 | }else{ 121 | win.show(); 122 | } 123 | }); 124 | } 125 | 126 | // Quit when all windows are closed. 127 | app.on('window-all-closed', () => { 128 | if (process.platform !== 'darwin') app.quit() 129 | }) 130 | 131 | // render operation 132 | app.whenReady().then(() => { 133 | 134 | const { ipcMain } = require('electron') 135 | 136 | // quit 137 | ipcMain.on('quit', () => { 138 | app.quit() 139 | }) 140 | 141 | // get config path 142 | ipcMain.handle('getConfigPath', () => { 143 | return path.join(app.getPath('home'), '.claude') 144 | }) 145 | 146 | // open config 147 | ipcMain.on('openConfig', () => { 148 | const configDir = path.join(app.getPath('home'), '.claude'); 149 | const { shell } = require('electron') 150 | shell.openPath(configDir).then(r => logger.info(r)) 151 | }) 152 | 153 | // get update info 154 | ipcMain.handle('getUpdateInfo', () => { 155 | let { updater } = getLocalData() 156 | let { auto, version: ver, skip } = updater || {} 157 | if (auto) { 158 | // file exists 159 | return { auto, ver, skip } 160 | }else{ 161 | // file not exists 162 | let updaterData = { 163 | version: app.getVersion(), 164 | skip: false, 165 | auto: false, 166 | } 167 | setLocalData({ 168 | updater: { 169 | ...updaterData, 170 | }, 171 | }) 172 | return { auto: false, version: app.getVersion() , skip: false } 173 | } 174 | }) 175 | 176 | // set update info 177 | ipcMain.handle('setUpdateInfo', (event, arg) => { 178 | setLocalData({ 179 | updater: { 180 | ...arg, 181 | }, 182 | }) 183 | return "ok"; 184 | }) 185 | 186 | // reset update info 187 | ipcMain.handle('resetUpdateInfo', () => { 188 | setLocalData({ 189 | updater: { 190 | version: app.getVersion(), 191 | skip: false, 192 | auto: false, 193 | }, 194 | }) 195 | return "ok" 196 | }) 197 | 198 | // get config 199 | ipcMain.handle('getConfig', () => { 200 | return ConfigFactory(); 201 | }) 202 | 203 | // update config 204 | ipcMain.handle('updateConfig', (event, arg) => { 205 | ConfigUpdate(arg); 206 | return "ok"; 207 | }) 208 | 209 | // reset config 210 | ipcMain.handle('resetConfig', () => { 211 | const config = new Config(); 212 | ConfigUpdate(config); 213 | return "ok" 214 | }) 215 | 216 | // get prompt url 217 | ipcMain.handle('getPromptURL', () => { 218 | return promptUrl; 219 | }) 220 | 221 | // open prompt 222 | ipcMain.on('openPrompt', () => { 223 | const { shell } = require('electron') 224 | shell.openExternal(config.prompt_path).then(r => logger.info(r)) 225 | }) 226 | 227 | // get prompt 228 | ipcMain.handle('getPrompt', () => { 229 | return downloadAndGetPrompt(config.prompt_path); 230 | }) 231 | 232 | // set prompt 233 | ipcMain.handle('setPrompt', (event, arg) => { 234 | setPromptInfo(config.prompt_path, arg); 235 | return "ok"; 236 | }) 237 | 238 | // reset prompt 239 | ipcMain.handle('resetPrompt', () => { 240 | return resetPromptInfo(config.prompt_path); 241 | }) 242 | 243 | // sync prompt 244 | ipcMain.handle('syncPrompt', () => { 245 | syncPromptInfo(config.prompt_path).then(r => logger.info(r)).catch(e => logger.error(e)); 246 | return "ok" 247 | }) 248 | 249 | // auto update 250 | autoUpdateInit().then((r: any) => logger.info(r)); 251 | 252 | }) -------------------------------------------------------------------------------- /src/views/ControlCenterView.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 142 | 143 | 270 | -------------------------------------------------------------------------------- /electron/menu.ts: -------------------------------------------------------------------------------- 1 | import {app, BrowserWindow, dialog, MenuItemConstructorOptions, nativeTheme} from 'electron' 2 | import configModel from "../public/configModel"; 3 | import path from "path"; 4 | import {ConfigFactory, ConfigUpdate} from "../public/config"; 5 | import {autoUpdater} from "electron-updater"; 6 | import logger from "electron-log"; 7 | 8 | let PromptWin: BrowserWindow | null = null; 9 | let ControlCenterWin: BrowserWindow | null = null; 10 | 11 | export default function createMenu(config: configModel) { 12 | 13 | // theme: light=0, dark=1, system=2 14 | let theme: number = 2; 15 | switch (config.theme){ 16 | case 'light': 17 | nativeTheme.themeSource = 'light'; 18 | theme = 0; 19 | break; 20 | case 'dark': 21 | nativeTheme.themeSource = 'dark'; 22 | theme = 1; 23 | break; 24 | case 'system': 25 | nativeTheme.themeSource = 'system'; 26 | theme = 2; 27 | break; 28 | } 29 | 30 | const menuTemplate: Array = [{ 31 | label: 'Claude', 32 | submenu: [ 33 | { 34 | label: 'About Claude', click: () => { 35 | dialog.showMessageBox({ 36 | icon: 'public/logo.png', 37 | title: 'About Claude', 38 | // package.json version 39 | message: `Version ${app.getVersion()}`, 40 | detail: 'Claude is a desktop app for the Claude chatbot.', 41 | buttons: ['OK'] 42 | }); 43 | } 44 | }, 45 | {label: 'Check for Updates', click: () => {autoUpdater.checkForUpdates().then(r => logger.info(r)).catch(e => logger.error(e))}, 46 | }, 47 | {role: 'minimize', label: 'Hide', accelerator: 'ctrl+H'}, 48 | {type: 'separator'}, 49 | {role: 'quit', label: 'Quit', accelerator: 'ctrl+W'} 50 | ] 51 | }, { 52 | label: 'Preferences', 53 | submenu: [ 54 | { 55 | label: 'Prompt', 56 | accelerator: 'ctrl+O', 57 | click: () => { 58 | if (PromptWin) { 59 | PromptWin.focus(); 60 | return; 61 | } 62 | // Create the browser window. 63 | PromptWin = new BrowserWindow({ 64 | title: 'Prompt Center', 65 | width: 800, 66 | height: 600, 67 | icon: "public/logo.png", 68 | modal: true, 69 | center: true, 70 | parent: BrowserWindow.getFocusedWindow(), 71 | webPreferences: { 72 | nodeIntegration: true, 73 | nodeIntegrationInWorker: true, 74 | webSecurity: false, 75 | preload: path.join(__dirname, '../electron/preload.js'), 76 | }, 77 | titleBarStyle: 'hidden', 78 | titleBarOverlay: { 79 | color: '#000000', 80 | symbolColor: '#74b1be', 81 | height: 30, 82 | } 83 | }) 84 | if (app.isPackaged) { 85 | PromptWin.loadFile(path.join(__dirname, '../dist/index.html'), { hash: 'prompt' }) 86 | } else { 87 | PromptWin.loadURL('http://localhost:5173/#/prompt') 88 | } 89 | // Emitted when the window is closed. 90 | PromptWin.on('closed', () => { 91 | PromptWin = null 92 | }) 93 | } 94 | },{ 95 | label: 'Control Center', 96 | accelerator: 'ctrl+shift+P', 97 | click: () => { 98 | // exist 99 | if (ControlCenterWin) { 100 | ControlCenterWin.focus(); 101 | return; 102 | } 103 | // Create the browser window. 104 | ControlCenterWin = new BrowserWindow({ 105 | title: 'Control Center', 106 | width: 1000, 107 | height: 650, 108 | resizable: false, 109 | icon: "public/logo.png", 110 | modal: true, 111 | center: true, 112 | parent: BrowserWindow.getFocusedWindow(), 113 | webPreferences: { 114 | nodeIntegration: true, 115 | nodeIntegrationInWorker: true, 116 | webSecurity: false, 117 | preload: path.join(__dirname, '../electron/preload.js') 118 | }, 119 | }) 120 | if (app.isPackaged) { 121 | ControlCenterWin.loadFile(path.join(__dirname, '../dist/index.html'), { hash: 'controlCenter' }) 122 | } else { 123 | ControlCenterWin.loadURL('http://localhost:5173/#/controlCenter') 124 | } 125 | // Emitted when the window is closed. 126 | ControlCenterWin.on('closed', () => { 127 | ControlCenterWin = null 128 | }) 129 | } 130 | }, 131 | {type: 'separator'}, 132 | { 133 | label: 'Stay On Top', type: 'checkbox', checked: config.stay_on_top, click: () => { 134 | let con = ConfigFactory(); 135 | con.stay_on_top = !con.stay_on_top; 136 | ConfigUpdate(con); 137 | const win = BrowserWindow.getFocusedWindow() 138 | if (win) { 139 | win.setAlwaysOnTop(con.stay_on_top); 140 | } 141 | }, accelerator: 'ctrl+T' 142 | }, 143 | { 144 | label: 'Theme', submenu: [ 145 | {label: 'Light', type: 'radio', checked: theme == 0,click: () => { 146 | let con = ConfigFactory(); 147 | con.theme = 'light'; 148 | ConfigUpdate(con); 149 | nativeTheme.themeSource = 'light'; 150 | }}, 151 | {label: 'Dark', type: 'radio', checked: theme == 1, click: () => { 152 | let con = ConfigFactory(); 153 | con.theme = 'dark'; 154 | ConfigUpdate(con); 155 | nativeTheme.themeSource = 'dark'; 156 | }}, 157 | {label: 'System', type: 'radio', checked: theme == 2, click: () => { 158 | let con = ConfigFactory(); 159 | con.theme = 'system'; 160 | ConfigUpdate(con); 161 | nativeTheme.themeSource = 'system'; 162 | }}, 163 | ] 164 | },] 165 | }, { 166 | label: 'Window', 167 | submenu: [ 168 | {role: 'minimize'}, 169 | {role: 'togglefullscreen'} 170 | ] 171 | }, { 172 | label: 'Edit', 173 | submenu: [ 174 | {role: 'undo'}, 175 | {role: 'redo'}, 176 | {type: 'separator'}, 177 | {role: 'copy'}, 178 | {role: 'paste'}, 179 | {type: 'separator'}, 180 | {role: 'selectAll'}, 181 | ] 182 | }, { 183 | label: 'View', 184 | submenu: [ 185 | {role: 'reload'}, 186 | {role: 'forceReload'}, 187 | {type: 'separator'}, 188 | {role: 'resetZoom'}, 189 | {role: 'zoomIn'}, 190 | {role: 'zoomOut'}, 191 | ] 192 | }, { 193 | role: 'help', 194 | submenu: [ 195 | { 196 | label: 'Learn More', 197 | click: async () => { 198 | const {shell} = require('electron') 199 | await shell.openExternal('https://github.com/Karenina-na/Claude-Desktop') 200 | } 201 | }, 202 | { 203 | label: 'About Author', 204 | click: async () => { 205 | const {shell} = require('electron') 206 | await shell.openExternal('https://github.com/Karenina-na') 207 | } 208 | }, 209 | {type: 'separator'}, 210 | {label: 'Toggle Dev Tools', accelerator: 'ctrl+shift+I', click: () => { 211 | const win = BrowserWindow.getFocusedWindow() 212 | if (win) { 213 | win.webContents.openDevTools({ mode: 'detach'}); 214 | } 215 | } 216 | }, 217 | ] 218 | } 219 | ] 220 | 221 | return menuTemplate; 222 | } -------------------------------------------------------------------------------- /src/components/ControlCenter/SettingsView.vue: -------------------------------------------------------------------------------- 1 | 165 | 166 | 333 | 334 | -------------------------------------------------------------------------------- /src/components/ControlCenter/PromptsView.vue: -------------------------------------------------------------------------------- 1 | 140 | 416 | 417 | --------------------------------------------------------------------------------