├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── coperation_issue.md │ └── feature_request.md ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.yaml ├── LICENSE ├── README.md ├── app-update.yml ├── dev-app-update.yml ├── electron.vite.config.ts ├── icons ├── icon.icns └── icon.ico ├── package.json ├── postcss.config.js ├── src ├── lib │ ├── langchain.ts │ ├── utils.ts │ └── utils_web.ts ├── main │ ├── eventHandler.ts │ ├── index.ts │ ├── lib │ │ ├── ai │ │ │ ├── embedding │ │ │ │ ├── embedding.ts │ │ │ │ ├── index.ts │ │ │ │ └── splitter.ts │ │ │ ├── fileLoader.ts │ │ │ ├── langchain.ts │ │ │ ├── parseURL.ts │ │ │ └── tts.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── models │ │ ├── default.ts │ │ ├── index.ts │ │ ├── memo.ts │ │ └── model.ts │ ├── service.ts │ └── window.ts ├── preload │ ├── index.d.ts │ └── index.ts └── renderer │ ├── index.html │ ├── public │ ├── katex.min.css │ └── tesseract.min.js │ └── src │ ├── App.tsx │ ├── assets │ ├── fonts │ │ ├── AlimamaFangYuanTiVF-Thin.woff2 │ │ └── MiSans-Normal.woff2 │ ├── icon │ │ ├── AnswerIcon.tsx │ │ ├── BrowserIcon.tsx │ │ ├── CapsuleIcon.tsx │ │ ├── ChatFullIcon.tsx │ │ ├── ChatIcon.tsx │ │ ├── CollectionIcon.tsx │ │ ├── EscIcon.tsx │ │ ├── InuseIcon.tsx │ │ ├── LinkIcon.tsx │ │ ├── NewChatIcon.tsx │ │ ├── SendIcon.tsx │ │ ├── SpeechIcon.tsx │ │ ├── StarIcon.tsx │ │ ├── TrashIcon.tsx │ │ ├── base │ │ │ ├── ClearIcon.tsx │ │ │ ├── CopyIcon.tsx │ │ │ ├── CrossMark.tsx │ │ │ ├── CrossMarkRound.tsx │ │ │ ├── DownloadIcon.tsx │ │ │ ├── Duplicate.tsx │ │ │ ├── EditIcon.tsx │ │ │ ├── EmptyIcon.tsx │ │ │ ├── HistoryIcon.tsx │ │ │ ├── MoreHIcon.tsx │ │ │ ├── MoreIcon.tsx │ │ │ ├── MoreVIcon.tsx │ │ │ ├── PauseIcon.tsx │ │ │ ├── PlayingIcon.tsx │ │ │ ├── Plus.tsx │ │ │ ├── QuestionMarkIcon.tsx │ │ │ ├── RefreshIcon.tsx │ │ │ ├── RetryIcon.tsx │ │ │ ├── ReturnIcon.tsx │ │ │ ├── SaveIcon.tsx │ │ │ ├── SearchIcon.tsx │ │ │ ├── SettingIcon.tsx │ │ │ ├── ShareIcon.tsx │ │ │ ├── Shift.tsx │ │ │ ├── StickTopIcon.tsx │ │ │ ├── Toast │ │ │ │ ├── ErrorIcon.tsx │ │ │ │ ├── SuccessIcon.tsx │ │ │ │ └── WarningIcon.tsx │ │ │ ├── UploadIcon.tsx │ │ │ ├── WithdrawalICon.tsx │ │ │ ├── arrow │ │ │ │ ├── DownwardArrow.tsx │ │ │ │ ├── LeftArrow.tsx │ │ │ │ ├── RightArrow.tsx │ │ │ │ └── UpwardArrow.tsx │ │ │ └── win │ │ │ │ ├── ExitFullScreenIcon.tsx │ │ │ │ ├── FullScreenIcon.tsx │ │ │ │ ├── MinimizeIcon.tsx │ │ │ │ └── WinCrossIcon.tsx │ │ ├── file │ │ │ └── baseFileIcon.tsx │ │ ├── models │ │ │ ├── ChatGptIcon.tsx │ │ │ ├── ClaudeIcon.tsx │ │ │ ├── CustomIcon.tsx │ │ │ ├── DeepSeekIcon.tsx │ │ │ ├── GeminiIcon.tsx │ │ │ ├── KimiIcon.tsx │ │ │ ├── LlamaIcon.tsx │ │ │ ├── OllamaIcon.tsx │ │ │ ├── QWenIcon.tsx │ │ │ └── WenxinIcon.tsx │ │ └── type.tsx │ └── index.css │ ├── components │ ├── Capsule │ │ └── index.tsx │ ├── MainInput │ │ ├── Context.tsx │ │ ├── Tools.tsx │ │ └── index.tsx │ ├── MainSelections │ │ ├── ModelSelect.tsx │ │ └── index.tsx │ ├── Message │ │ ├── Actions.tsx │ │ ├── GlobalSearch.tsx │ │ ├── Md.tsx │ │ ├── SpecialTypeContent.tsx │ │ └── index.tsx │ ├── ScrollBox.tsx │ ├── TopBar.tsx │ └── ui │ │ ├── BotIcon.tsx │ │ ├── Button.tsx │ │ ├── CapitalIcon.tsx │ │ ├── Card.tsx │ │ ├── CheckItem.tsx │ │ ├── Collapse.tsx │ │ ├── DoubleConfirm.tsx │ │ ├── DynamicLoading.tsx │ │ ├── EditInput.tsx │ │ ├── Expand.tsx │ │ ├── FilePicker.tsx │ │ ├── Loading.tsx │ │ ├── QuestionMention.tsx │ │ ├── Search.tsx │ │ ├── SegmentedControl.tsx │ │ ├── Select.tsx │ │ ├── Slider.tsx │ │ ├── SwitchItem.tsx │ │ ├── Toast.tsx │ │ ├── ToolTip.tsx │ │ ├── compWithTip.tsx │ │ └── style.css │ ├── env.d.ts │ ├── lib │ ├── ai │ │ ├── file │ │ │ └── index.ts │ │ ├── langchain │ │ │ ├── index.ts │ │ │ └── models.ts │ │ ├── memo │ │ │ └── index.ts │ │ ├── ocr.ts │ │ ├── parseString.ts │ │ ├── search │ │ │ └── index.ts │ │ ├── tts.ts │ │ └── url │ │ │ └── index.ts │ ├── constant.ts │ ├── html2canvas.js │ ├── md │ │ └── exportRecord.ts │ └── util.ts │ ├── main.tsx │ ├── pages │ ├── Answer │ │ ├── SelectAssistantModel.tsx │ │ └── index.tsx │ ├── Assistants │ │ ├── EditBox.tsx │ │ └── index.tsx │ ├── Chat.tsx │ ├── History │ │ ├── Collection.tsx │ │ ├── SpecialTypeContent.tsx │ │ ├── index.tsx │ │ └── utils.ts │ ├── Loading.tsx │ ├── Memo │ │ ├── EditBox.tsx │ │ └── index.tsx │ ├── Setting │ │ ├── Custom.tsx │ │ ├── VersionDesc.tsx │ │ ├── index.tsx │ │ └── theme.ts │ └── System.tsx │ └── store │ ├── answer.ts │ ├── assistants.ts │ ├── chat.ts │ ├── collection.ts │ ├── history.ts │ ├── input.ts │ ├── memo.ts │ ├── setting.ts │ └── user.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.web.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['solid', 'import'], 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:solid/typescript', 6 | '@electron-toolkit/eslint-config-ts/recommended', 7 | '@electron-toolkit/eslint-config-prettier' 8 | ], 9 | ignorePatterns: [ 10 | '**/{node_modules,lib,es,examples,output}', 11 | '**/*.d.ts', 12 | '**/tsconfig.json', 13 | 'src/renderer/public/**.js' 14 | ], 15 | 16 | rules: { 17 | '@typescript-eslint/no-explicit-any': [1], 18 | 'import/no-unresolved': [0], 19 | 'import/named': [1], 20 | 'import/unambiguous': [0], 21 | '@typescript-eslint/no-unused-vars': [1, { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 22 | 'import/extensions': [0], 23 | 'init-declarations': [0], 24 | '@typescript-eslint/init-declarations': [0], 25 | 'max-statements-per-line': [0], 26 | '@typescript-eslint/no-use-before-define': [1], 27 | '@typescript-eslint/explicit-function-return-type': [0], 28 | '@typescript-eslint/ban-ts-comment': [0], 29 | 'prefer-const': [1], 30 | "prettier/prettier": [0], 31 | 'import/no-extraneous-dependencies': [2], 32 | 'import/order': [ 33 | 'warn', 34 | { 35 | /* 导出顺序规则: 36 | 1. 内置的 Node.js 模块 37 | 2. 第三方模块(如 react,lodash 等) 38 | 3. 自定义的全局模块(在你的项目中定义的,但在多个文件中使用的模块) 39 | 4. 模块从父级目录导入(使用 ../) 40 | 5. 模块从同级或子级目录导入(使用 ./) */ 41 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling'], 42 | 'newlines-between': 'always' 43 | } 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | tesseract.min.js linguist-generated=true 2 | html2canvas.js linguist-generated=true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 出现 Bug 了 3 | about: 创建报告以帮助 Gomooon 改进 4 | --- 5 | 6 | ### 描述错误和预期行为 7 | 8 | 请简要描述您遇到的问题,以及您期望发生的情况。 9 | 10 | ### 截图 11 | 12 | 如果有能力,请添加截图以帮助说明问题。 13 | 14 | ### 环境信息 (重要⭐️) 15 | 16 | - Gomooon 版本: [例如 1.2.0] 17 | 18 | - 操作系统:(Mac 14.1 / Windows 11) 19 | 20 | - 其他特殊环境信息:(例如:使用代理,使用 Docker 等) 21 | 22 | ### 其他信息 23 | 24 | 其他您认为相关的信息。 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/coperation_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🌌 商务合作 3 | about: 如果您期望为您的组织私有化部署,功能定制化,请使用此模板 4 | --- 5 | 6 | ### 组织信息 7 | 8 | 请大致描述您的组织信息。 9 | 10 | ### 具体需求 11 | 12 | 请描述您的具体需求,比如: 13 | 14 | - 您的组织希望私有化部署,功能定制化。 15 | - 您希望我们提供技术支持。 16 | 17 | ### 其他信息 18 | 19 | 其他您认为相关的信息。 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 新功能请求 3 | about: 提出你的 idea,以帮助我们改进 Gomoon 4 | --- 5 | 6 | ### 功能描述 7 | 8 | 请描述您想要的功能。 9 | 10 | ### 如何实现 11 | 12 | 提供一些实现建议或想法。 13 | 14 | ### 其他信息 15 | 16 | 其他您认为相关的信息。 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Referenced from https://github.com/github/gitignore/blob/master/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | 65 | # other stuff 66 | .DS_Store 67 | Thumbs.db 68 | 69 | # IDE configurations 70 | .idea 71 | .vscode 72 | 73 | # build assets 74 | /output 75 | /dist 76 | /dll 77 | /build-cache 78 | 79 | #yarn 80 | .pnp.* 81 | .yarn/* 82 | !.yarn/patches 83 | !.yarn/plugins 84 | !.yarn/releases 85 | !.yarn/sdks 86 | !.yarn/versions 87 | 88 | /out 89 | src/renderer/src/lib/config.json 90 | 91 | other 92 | resources 93 | extents 94 | 95 | electron-builder.yml -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | nodeLinker=node-modules 2 | registry="https://registry.npmmirror.com/" 3 | electron_mirror="https://registry.npmmirror.com/-/binary/electron/" 4 | electron_builder_binaries_mirror="https://registry.npmmirror.com/-/binary/electron-builder-binaries/" -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | printWidth: 100 4 | trailingComma: none 5 | plugins: 6 | - prettier-plugin-tailwindcss 7 | -------------------------------------------------------------------------------- /app-update.yml: -------------------------------------------------------------------------------- 1 | provider: generic 2 | url: https://vip.123pan.cn/1830083732/update 3 | -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | provider: generic 2 | url: https://vip.123pan.cn/1830083732/update 3 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | 3 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 4 | import solid from 'vite-plugin-solid' 5 | 6 | export default defineConfig({ 7 | main: { 8 | plugins: [ 9 | externalizeDepsPlugin({ 10 | exclude: ['lowdb'] 11 | }) 12 | ], 13 | build: { 14 | rollupOptions: { 15 | output: { 16 | manualChunks(id) { 17 | if (id.includes('lowdb')) { 18 | return 'lowdb' 19 | } 20 | return undefined // let rollup handle all other node_modules 21 | } 22 | } 23 | } 24 | } 25 | }, 26 | preload: { 27 | plugins: [externalizeDepsPlugin()] 28 | }, 29 | renderer: { 30 | resolve: { 31 | alias: { 32 | '@renderer': resolve('src/renderer/src'), 33 | '@lib': resolve('src/lib') 34 | } 35 | }, 36 | // public 目录下的文件可以直接通过 / 访问 37 | publicDir: resolve('src/renderer/public'), 38 | plugins: [solid()] 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardAEI/Gomoon/39885fb2f66e1b6f848213e4adab7464e9efee08/icons/icon.icns -------------------------------------------------------------------------------- /icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardAEI/Gomoon/39885fb2f66e1b6f848213e4adab7464e9efee08/icons/icon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gomoon", 3 | "productName": "Gomoon", 4 | "version": "1.3.3", 5 | "description": "An ai tools for everyone on Mac, Windows and Linux.", 6 | "main": "./out/main/index.js", 7 | "author": "aei", 8 | "license": "Apache-2.0", 9 | "scripts": { 10 | "format": "prettier --write .", 11 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 12 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 13 | "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", 14 | "typecheck": "npm run typecheck:node && npm run typecheck:web", 15 | "start": "electron-vite preview", 16 | "dev": "electron-vite dev", 17 | "build": "npm run typecheck && electron-vite build", 18 | "postinstall": "electron-builder install-app-deps", 19 | "build:win": "electron-vite build && electron-builder --win --config", 20 | "build:mac": "electron-vite build && electron-builder --mac --config", 21 | "build:linux": "electron-vite build && electron-builder --linux --config" 22 | }, 23 | "dependencies": { 24 | "@dicebear/collection": "^9.2.1", 25 | "@dicebear/core": "^9.2.1", 26 | "@electron-toolkit/preload": "^3.0.1", 27 | "@electron-toolkit/utils": "^3.0.0", 28 | "@google/generative-ai": "^0.14.1", 29 | "@langchain/anthropic": "^0.2.12", 30 | "@langchain/baidu-qianfan": "^0.1.0", 31 | "@langchain/community": "^0.2.20", 32 | "@langchain/core": "^0.2.17", 33 | "@langchain/deepseek": "^0.0.1", 34 | "@langchain/google-genai": "^0.0.23", 35 | "@langchain/ollama": "^0.1.0", 36 | "@langchain/openai": "^0.2.4", 37 | "@mozilla/readability": "^0.5.0", 38 | "@solidjs/router": "^0.13.2", 39 | "@types/markdown-it": "^14.0.1", 40 | "@types/prismjs": "^1.26.3", 41 | "@types/ws": "^8.5.10", 42 | "@vscode/markdown-it-katex": "^1.0.3", 43 | "@xenova/transformers": "^2.17.1", 44 | "@zag-js/checkbox": "^0.50.0", 45 | "@zag-js/popper": "^0.50.0", 46 | "@zag-js/radio-group": "^0.50.0", 47 | "@zag-js/solid": "^0.50.0", 48 | "@zag-js/switch": "^0.50.0", 49 | "@zag-js/tooltip": "^0.50.0", 50 | "apache-arrow": "^16.0.0", 51 | "cheerio": "^1.0.0-rc.12", 52 | "d3-dsv": "2", 53 | "electron-updater": "^6.1.8", 54 | "highlight.js": "^11.9.0", 55 | "jsdom": "^24.0.0", 56 | "langchain": "^0.1.36", 57 | "libreoffice-convert": "^1.5.1", 58 | "lodash": "^4.17.21", 59 | "lowdb": "^7.0.1", 60 | "mammoth": "^1.7.1", 61 | "markdown-it": "^14.1.0", 62 | "markdown-it-emoji": "^3.0.0", 63 | "markdown-it-highlightjs": "^4.0.1", 64 | "mermaid": "^11.2.1", 65 | "moment": "^2.30.1", 66 | "node-fetch": "^2.6.4", 67 | "node-llama-cpp": "^2.8.9", 68 | "officeparser": "^4.0.8", 69 | "pdf-parse": "^1.1.1", 70 | "robotjs": "^0.6.0", 71 | "solid-js": "^1.8.17", 72 | "solidjs-use": "^2.3.0", 73 | "tesseract.js": "^5.0.5", 74 | "ulid": "^2.3.0", 75 | "vectordb": "^0.4.16", 76 | "ws": "^8.16.0", 77 | "xlsx": "^0.18.5" 78 | }, 79 | "devDependencies": { 80 | "@electron-toolkit/eslint-config-prettier": "^1.0.1", 81 | "@electron-toolkit/eslint-config-ts": "^1.0.0", 82 | "@electron-toolkit/tsconfig": "^1.0.1", 83 | "@types/lodash": "^4.17.0", 84 | "@types/node": "^20.12.7", 85 | "autoprefixer": "^10.4.19", 86 | "electron-builder": "^24.13.3", 87 | "electron-vite": "^2.2.0", 88 | "eslint": "^8.47.0", 89 | "eslint-plugin-import": "^2.29.1", 90 | "eslint-plugin-solid": "^0.13.2", 91 | "postcss": "^8.4.38", 92 | "prebuild-install": "^7.1.2", 93 | "prettier": "^3.2.5", 94 | "prettier-plugin-tailwindcss": "^0.5.14", 95 | "tailwindcss": "^3.4.3", 96 | "tailwindcss-themer": "^4.0.0", 97 | "typescript": "^5.4.5", 98 | "vite": "^5.2.10", 99 | "electron": "29.3.1", 100 | "vite-plugin-solid": "^2.10.2" 101 | }, 102 | "packageManager": "yarn@1.22.22" 103 | } 104 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | 3 | import { ModelInterfaceType, Models, ModelsType, loadLMMap } from './langchain' 4 | 5 | export const newChatLlamaForNode = async (config: { src: string; temperature: number }) => { 6 | const { ChatLlamaCpp } = await import('@langchain/community/chat_models/llama_cpp') 7 | // 检查模型文件是否存在 8 | if (!config.src || !existsSync(config.src)) { 9 | return { 10 | invoke() { 11 | throw new Error('Llama model not found') 12 | }, 13 | stream() { 14 | throw new Error('Llama model not found') 15 | } 16 | } 17 | } 18 | return new ChatLlamaCpp({ 19 | modelPath: config.src, 20 | temperature: config.temperature, 21 | gpuLayers: 64 22 | }) 23 | } 24 | 25 | export const loadLMMapForNode = async ( 26 | model: Models 27 | ): Promise<{ 28 | [key in ModelsType]: ModelInterfaceType 29 | }> => { 30 | const modelMap = await loadLMMap(model) 31 | if (!model.Llama.src) return modelMap 32 | try { 33 | modelMap.Llama = await newChatLlamaForNode(model.Llama) 34 | } catch (e: unknown) { 35 | modelMap.Llama = { 36 | invoke() { 37 | throw e as Error 38 | }, 39 | stream() { 40 | throw e as Error 41 | } 42 | } 43 | } 44 | return modelMap 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/utils_web.ts: -------------------------------------------------------------------------------- 1 | function base64ToBlob(base64, mimeType) { 2 | // 解码Base64字符串 3 | const byteCharacters = atob(base64) 4 | // 创建一个8位的视图数组 5 | const byteNumbers = new Array(byteCharacters.length) 6 | for (let i = 0; i < byteCharacters.length; i++) { 7 | byteNumbers[i] = byteCharacters.charCodeAt(i) 8 | } 9 | // 将8位视图数组转换为Uint8Array 10 | const byteArray = new Uint8Array(byteNumbers) 11 | // 创建Blob对象 12 | return new Blob([byteArray], { type: mimeType }) 13 | } 14 | 15 | export function base64ToFile(base64, fileName) { 16 | // 从Base64编码的URL中提取Base64字符串 17 | const base64Data = base64.split(',')[1] 18 | // 确定MIME类型 19 | const mimeMatch = base64.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9]+).*,.*/) 20 | const mimeType = (mimeMatch ? (mimeMatch[1] ? mimeMatch[1] : '') : '') as string 21 | 22 | // 转换为Blob对象 23 | const blob = base64ToBlob(base64Data, mimeType) 24 | 25 | // 创建File对象 26 | return new File([blob], `${fileName}.${mimeType.split('/')[1] || 'png'}`, { type: mimeType }) 27 | } 28 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu } from 'electron' 2 | import { electronApp, optimizer } from '@electron-toolkit/utils' 3 | import robot from 'robotjs' 4 | 5 | import icon from '../../resources/icon.png?asset' 6 | 7 | import { initAppEventsHandler } from './eventHandler' 8 | import { createWindow, showWindow } from './window' 9 | import { quitApp } from './lib' 10 | import { activateTokenizer } from './lib/ai/embedding/embedding' 11 | 12 | // dock 13 | app.dock?.setIcon(icon) 14 | app.dock?.setMenu(Menu.buildFromTemplate([])) 15 | 16 | // 检测只启动一个app 17 | const gotTheLock = app.requestSingleInstanceLock() 18 | 19 | if (!gotTheLock) { 20 | // 如果获取锁失败,说明已经有一个实例在运行了,可以直接退出 21 | app.quit() 22 | } else { 23 | app.on('second-instance', () => { 24 | // 当运行第二个实例时,将会聚焦到 myWindow 这个窗口 25 | showWindow() 26 | }) 27 | } 28 | 29 | app.whenReady().then(() => { 30 | // Set app user model id for windows 31 | electronApp.setAppUserModelId('com.Gomoon') 32 | 33 | // Default open or close DevTools by F12 in development 34 | // and ignore CommandOrControl + R in production. 35 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 36 | app.on('browser-window-created', (_, window) => { 37 | optimizer.watchWindowShortcuts(window) 38 | }) 39 | 40 | initAppEventsHandler() 41 | createWindow() 42 | 43 | app.on('activate', function () { 44 | // On macOS it's common to re-create a window in the app when the 45 | // dock icon is clicked and there are no other windows open. 46 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 47 | }) 48 | }) 49 | 50 | app.on('before-quit', async (e) => { 51 | // Unregister all shortcuts. 52 | if (quitApp.shouldQuit) { 53 | return 54 | } 55 | e.preventDefault() 56 | await quitApp.quit() 57 | setTimeout(() => app.quit()) 58 | }) 59 | 60 | app.on('window-all-closed', () => { 61 | if (process.platform !== 'darwin') { 62 | app.quit() 63 | } 64 | }) 65 | 66 | // 激活robot 67 | // 初始化tokenizer 68 | // 分出一个线程,防止阻塞主进程 69 | setTimeout(() => { 70 | const pos = robot.getMousePos() 71 | robot.moveMouse(pos.x, pos.y) 72 | activateTokenizer() 73 | }) 74 | -------------------------------------------------------------------------------- /src/main/lib/ai/embedding/embedding.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { existsSync, mkdirSync, rmdirSync, writeFileSync } from 'fs' 3 | 4 | import { app } from 'electron' 5 | 6 | const appDataPath = app.getPath('userData') 7 | 8 | export function getEmbeddingModel() { 9 | return 'embedding/Xenova/jina-embeddings-v2-base-zh' 10 | } 11 | 12 | export async function embedding(text: string): Promise { 13 | const { env, pipeline } = await import('@xenova/transformers') 14 | env.allowLocalModels = true 15 | env.localModelPath = appDataPath 16 | env.backends.onnx.wasm.numThreads = 1 17 | env.backends.onnx.logLevel = 'info' 18 | const extractor = await pipeline('feature-extraction', getEmbeddingModel(), { 19 | model_file_name: 'model', 20 | local_files_only: true 21 | }) 22 | const output = await extractor(text, { pooling: 'mean', normalize: true }) 23 | return (output?.data as Float32Array) || [] 24 | } 25 | 26 | let haveActivatedTokenizer = false 27 | export async function activateTokenizer() { 28 | if (haveActivatedTokenizer) { 29 | return 30 | } 31 | if (existsSync(join(appDataPath, getEmbeddingModel()))) { 32 | haveActivatedTokenizer = true 33 | return 34 | } 35 | mkdirSync(join(appDataPath, getEmbeddingModel()), { 36 | recursive: true 37 | }) 38 | const url = 'https://vip.123pan.cn/1830083732/update/embedding/Xenova/jina-embeddings-v2-base-zh/' 39 | const fileList = ['config.json', 'tokenizer.json', 'tokenizer_config.json'] 40 | try { 41 | await Promise.all( 42 | fileList.map(async (fileName) => { 43 | const filePath = join(appDataPath, getEmbeddingModel(), fileName) 44 | const response = await fetch(url + fileName) 45 | if (response.status === 200) { 46 | const buffer = Buffer.from(await response.arrayBuffer()) 47 | writeFileSync(filePath, buffer) 48 | } else { 49 | throw new Error('Failed to download file: ' + fileName) 50 | } 51 | await new Promise((resolve) => setTimeout(resolve, 1000)) 52 | }) 53 | ) 54 | } catch (e) { 55 | rmdirSync(join(appDataPath, getEmbeddingModel()), { 56 | recursive: true 57 | }) 58 | } 59 | haveActivatedTokenizer = true 60 | } 61 | 62 | export async function tokenize(text: string) { 63 | if (!haveActivatedTokenizer) { 64 | return [] 65 | } 66 | const { AutoTokenizer, env } = await import('@xenova/transformers') 67 | env.allowLocalModels = true 68 | env.localModelPath = appDataPath 69 | env.backends.onnx.wasm.numThreads = 1 70 | env.backends.onnx.logLevel = 'info' 71 | try { 72 | const tokenizer = await AutoTokenizer.from_pretrained(getEmbeddingModel()) 73 | // Run tokenization 74 | const text_inputs = tokenizer.encode(text) 75 | return text_inputs 76 | } catch (e) { 77 | console.error(e) 78 | return [] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/lib/ai/langchain.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events' 2 | 3 | import { ChatLlamaCpp } from '@langchain/community/chat_models/llama_cpp' 4 | 5 | import { msgDict } from '../../../lib/langchain' 6 | import { loadLMMapForNode } from '../../../lib/utils' 7 | import { getUserData, loadAppConfig } from '../../models' 8 | import { postMsgToMainWindow } from '../../window' 9 | 10 | const emit = new EventEmitter() 11 | 12 | function getLMConfig() { 13 | return { 14 | models: loadAppConfig().models, 15 | current: getUserData().selectedModel 16 | } 17 | } 18 | 19 | export interface CallLLmOption { 20 | llm: string 21 | modelName?: string 22 | msgs: { 23 | role: 'human' | 'system' | 'ai' 24 | content: string 25 | }[] 26 | type: 'ans' | 'chat' 27 | } 28 | 29 | export async function callLLM(options: CallLLmOption) { 30 | if (options.llm === 'Llama') { 31 | const controller = new AbortController() 32 | emit.once('abort', () => { 33 | console.log('abort') 34 | controller.abort() 35 | }) 36 | const llm = (await loadLMMapForNode(getLMConfig().models))['Llama'] as ChatLlamaCpp 37 | await llm.invoke( 38 | options.msgs.map((msg) => msgDict[msg.role](msg.content)), 39 | { 40 | callbacks: [ 41 | { 42 | handleLLMNewToken(output) { 43 | postMsgToMainWindow(`new content: ${output}`) 44 | }, 45 | 46 | handleLLMError(error) { 47 | throw error 48 | } 49 | } 50 | ], 51 | signal: controller.signal 52 | } 53 | ) 54 | } 55 | } 56 | 57 | export async function stopLLM() { 58 | emit.emit('abort') 59 | } 60 | 61 | export async function lmInvoke(option: { system?: string; content: string }): Promise { 62 | const lm = (await loadLMMapForNode(getLMConfig().models))[getLMConfig().current] 63 | if (!lm) { 64 | throw new Error('llm not found') 65 | } 66 | const msgs = [msgDict['human'](option.content)] 67 | if (option.system) msgs.unshift(msgDict['system'](option.system)) 68 | try { 69 | const res = await lm.invoke(msgs) 70 | return res.content as string 71 | } catch (e: unknown) { 72 | throw new Error('llm error', e as Error) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/lib/ai/parseURL.ts: -------------------------------------------------------------------------------- 1 | // import puppeteer from 'puppeteer' 2 | import fetch from 'node-fetch' 3 | import { load } from 'cheerio' 4 | import { Readability } from '@mozilla/readability' 5 | import { JSDOM } from 'jsdom' 6 | import { BrowserWindow } from 'electron' 7 | // TODO: 转md 8 | export async function parseURL2Str(url: string): Promise { 9 | const html = await fetch(url, { 10 | timeout: 10000 11 | }) 12 | .then((res) => res.text()) 13 | .catch((e) => { 14 | return e.message 15 | }) 16 | let $ = load(html) 17 | let doc = new Readability(new JSDOM($.html()).window.document).parse() 18 | if (doc && doc?.textContent.length > 150 && doc.siteName !== '微信公众平台') { 19 | return `${doc.title}\n\n` + doc.textContent 20 | } 21 | 22 | let tempWin = new BrowserWindow({ 23 | width: 800, 24 | height: 600, 25 | webPreferences: { 26 | nodeIntegration: true 27 | }, 28 | show: false 29 | }) 30 | tempWin.minimize() 31 | try { 32 | tempWin.loadURL(url) 33 | const res = await tempWin.webContents.executeJavaScript('document.documentElement.outerHTML') 34 | $ = load(res) 35 | doc = new Readability(new JSDOM($.html()).window.document).parse() 36 | if (doc && doc?.textContent.length > 150) { 37 | return `${doc.title}\n\n` + doc.textContent 38 | } 39 | } finally { 40 | tempWin.close() 41 | // @ts-ignore 42 | tempWin = null 43 | } 44 | return '无效网页' 45 | } 46 | -------------------------------------------------------------------------------- /src/main/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { writeFile } from 'fs' 3 | 4 | import { app, dialog, globalShortcut } from 'electron' 5 | 6 | import { beforeQuitWindowHandler } from '../window' 7 | 8 | export function getResourcesPath(filename: string): string { 9 | return app.isPackaged 10 | ? join(process.resourcesPath, '/app.asar.unpacked/resources/' + filename) 11 | : join(__dirname, '../../resources/', filename) 12 | } 13 | 14 | export const quitApp = { 15 | shouldQuit: false, 16 | async quit() { 17 | await beforeQuitWindowHandler() 18 | globalShortcut.unregisterAll() 19 | this.shouldQuit = true 20 | }, 21 | reset() { 22 | this.shouldQuit = false 23 | } 24 | } 25 | export async function saveFile(fileName: string, content: string | Buffer) { 26 | const res = await dialog.showSaveDialog({ 27 | title: '保存文件', 28 | buttonLabel: '保存', 29 | defaultPath: fileName, 30 | filters: [ 31 | { 32 | name: 'All Files', 33 | extensions: ['*'] 34 | } 35 | ] 36 | }) 37 | if (res.filePath) { 38 | writeFile(res.filePath, content, () => {}) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from 'stream' 2 | 3 | import fetch from 'node-fetch' 4 | 5 | export function isValidUrl(url: string) { 6 | if (!url) return false 7 | try { 8 | new URL(url) 9 | return true 10 | } catch (e) { 11 | return false 12 | } 13 | } 14 | 15 | export async function fetchWithProgress(url, onProgress) { 16 | const response = await fetch(url) 17 | 18 | if (!response.ok) { 19 | throw new Error(`HTTP error! status: ${response.status}`) 20 | } 21 | 22 | const contentLength = response.headers.get('Content-Length') 23 | if (!contentLength) { 24 | throw new Error('Content-Length header is missing') 25 | } 26 | 27 | const total = parseInt(contentLength, 10) 28 | let loaded = 0 29 | const chunks: any[] = [] 30 | 31 | const writableStream = new Writable({ 32 | write(chunk, _, callback) { 33 | chunks.push(chunk) 34 | loaded += chunk.length 35 | onProgress(loaded, total) 36 | callback() 37 | } 38 | }) 39 | 40 | response!.body.pipe(writableStream) 41 | 42 | await new Promise((resolve, reject) => { 43 | writableStream.on('finish', resolve) 44 | writableStream.on('error', reject) 45 | }) 46 | 47 | return Buffer.concat(chunks) 48 | } 49 | -------------------------------------------------------------------------------- /src/main/models/default.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import os from 'os' 3 | 4 | import { getResourcesPath } from '../lib' 5 | import { defaultModels } from '../../lib/langchain' 6 | 7 | import { AssistantModel, Line, MemoModel, UserDataModel } from './model' 8 | import { SettingModel } from './model' 9 | import { ImportMemoDataModel } from './memo' 10 | 11 | export function getDefaultAssistants(): AssistantModel[] { 12 | const a = readFileSync(getResourcesPath('assistants.json'), 'utf-8') 13 | const { assistants } = JSON.parse(a) 14 | return assistants 15 | } 16 | 17 | export function getDefaultConfig(): SettingModel { 18 | return { 19 | isOnTop: false, 20 | models: defaultModels(), 21 | quicklyAnsKey: 'C', 22 | quicklyWakeUpKeys: os.platform() === 'darwin' ? 'Cmd+G' : 'Ctrl+G', 23 | sendWithCmdOrCtrl: true, 24 | theme: 'gomoon-theme', 25 | chatFontSize: 14, 26 | openAtLogin: false, 27 | fontFamily: 'default' 28 | } 29 | } 30 | 31 | export function getDefaultLines(): Line[] { 32 | const lines = readFileSync(getResourcesPath('lines.json'), 'utf-8') 33 | const ls = JSON.parse(lines) 34 | return ls 35 | } 36 | 37 | export function getDefaultUserData(): UserDataModel { 38 | const a = readFileSync(getResourcesPath('assistants.json'), 'utf-8') 39 | const memo = readFileSync(getResourcesPath('memories.json'), 'utf-8') 40 | 41 | const { selectedAssistantForAns, selectedAssistantForChat } = JSON.parse(a) 42 | const { selectedMemo } = JSON.parse(memo) 43 | return { 44 | firstTime: true, 45 | selectedModel: 'GPT4', 46 | selectedAssistantForAns, 47 | selectedAssistantForChat, 48 | selectedMemo, 49 | firstTimeFor: { 50 | modelSelect: true, 51 | assistantSelect: true 52 | }, 53 | windowSize: { 54 | width: 480, 55 | height: 760 56 | } 57 | } 58 | } 59 | 60 | export function getDefaultMemories(): { 61 | memo: MemoModel 62 | data: { 63 | [id: string]: ImportMemoDataModel 64 | } 65 | }[] { 66 | const memo = readFileSync(getResourcesPath('memories.json'), 'utf-8') 67 | const { memories } = JSON.parse(memo) 68 | return memories 69 | } 70 | -------------------------------------------------------------------------------- /src/main/models/model.ts: -------------------------------------------------------------------------------- 1 | import { Models, ModelsType } from '../../lib/langchain' 2 | 3 | export interface UserDataModel { 4 | firstTime: boolean 5 | selectedModel: ModelsType 6 | selectedAssistantForChat: string 7 | selectedAssistantForAns: string 8 | selectedMemo: string 9 | firstTimeFor: { 10 | modelSelect?: boolean 11 | assistantSelect?: boolean 12 | } 13 | windowSize: { 14 | width: number 15 | height: number 16 | } 17 | } 18 | 19 | export type AssistantType = 'chat' | 'answer' 20 | export type ToolEnum = 'memory' | 'file' | 'image' | 'voice' | 'video' 21 | export type AssistantModel = ( 22 | | { 23 | type: 'chat' 24 | } 25 | | { 26 | type: 'ans' 27 | prompts?: string[] 28 | } 29 | ) & { 30 | id: string 31 | version: number 32 | avatar?: string 33 | name: string 34 | introduce?: string 35 | prompt: string 36 | matchModel?: ModelsType | 'current' 37 | // 保留字段 38 | deleted?: boolean 39 | tools?: ToolEnum[] 40 | } 41 | export type CreateAssistantModel = Omit & { version?: number } 42 | export interface HistoryModel { 43 | id: string 44 | type: 'chat' | 'ans' 45 | assistantId?: string 46 | starred?: boolean 47 | contents: { id?: string; role: 'human' | 'system' | 'ai' | 'ans' | 'question'; content: string }[] 48 | } 49 | export interface CollectionModel { 50 | id: string 51 | name: string 52 | contents: { 53 | id?: string 54 | type: 'chat' | 'ans' 55 | assistantId: string 56 | role: 'human' | 'system' | 'ai' | 'ans' | 'question' 57 | content: string 58 | }[][] 59 | } 60 | export type CreateCollectionModel = Omit 61 | 62 | export interface Line { 63 | content: string 64 | from: string 65 | } 66 | export interface MemoFragment { 67 | type: 'md' | 'xlsx' 68 | name: string 69 | from?: string 70 | } 71 | export interface MemoFragmentData { 72 | id: string 73 | name: string 74 | data: string 75 | embeddingModel: string 76 | vectors: Float32Array[] 77 | indexes: string[] 78 | } 79 | export interface MemoModel { 80 | id: string 81 | version: number 82 | name: string 83 | introduce?: string 84 | fragment: MemoFragment[] 85 | } 86 | export type CreateMemoModel = Omit & { version?: number } 87 | 88 | export type SettingFontFamily = 'default' | 'MiSans' | 'AlimamaFangyuan' 89 | export interface SettingModel { 90 | isOnTop: boolean 91 | quicklyAnsKey: string 92 | quicklyWakeUpKeys: string 93 | sendWithCmdOrCtrl: boolean 94 | models: Models 95 | theme: string 96 | chatFontSize: number 97 | fontFamily: SettingFontFamily 98 | openAtLogin: boolean 99 | } 100 | export interface MemoResult { 101 | content: string 102 | } 103 | -------------------------------------------------------------------------------- /src/main/service.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync, writeFileSync } from 'fs' 2 | import { join } from 'path' 3 | 4 | import { autoUpdater } from 'electron-updater' 5 | import { app } from 'electron' 6 | 7 | import { fetchWithProgress } from './lib/utils' 8 | import { postMsgToMainWindow } from './window' 9 | import { saveFile } from './lib' 10 | import { getEmbeddingModel } from './lib/ai/embedding/embedding' 11 | import { getMemories, initMemories } from './models' 12 | export async function updateForMac() { 13 | const res = await autoUpdater.checkForUpdates() 14 | if (res === null) { 15 | return 16 | } 17 | let url = 'https://vip.123pan.cn/1830083732/update/' 18 | const fileName = 19 | 'gomoon-' + res?.updateInfo?.version + (process.arch === 'x64' ? '-x64' : '-arm64') + '.dmg' 20 | url += fileName 21 | if (!res.updateInfo.version || !res.updateInfo.releaseDate) { 22 | return 23 | } 24 | fetchWithProgress(url, (loaded, total) => { 25 | postMsgToMainWindow('download-progress ' + Math.ceil((loaded / total) * 100)) 26 | }) 27 | .then((buf) => { 28 | postMsgToMainWindow('update-downloaded') 29 | saveFile(fileName, buf) 30 | }) 31 | .catch((error) => { 32 | console.error('Fetch error:', error) 33 | }) 34 | } 35 | 36 | export function checkModelsSvc() { 37 | return ( 38 | existsSync(join(app.getPath('userData'), getEmbeddingModel(), 'onnx')) && 39 | getMemories().length > 0 40 | ) 41 | } 42 | 43 | export async function initMemoriesSvc() { 44 | const modelPath = join(app.getPath('userData'), getEmbeddingModel(), 'onnx') 45 | if (!existsSync(modelPath)) { 46 | mkdirSync(modelPath, { recursive: true }) 47 | const url = 48 | 'https://vip.123pan.cn/1830083732/update/embedding/Xenova/jina-embeddings-v2-base-zh/onnx/model_quantized.onnx' 49 | const buf = await fetchWithProgress(url, (loaded, total) => { 50 | postMsgToMainWindow('model-progress ' + `${Math.ceil((loaded / total) * 100)}%`) 51 | }) 52 | writeFileSync(join(modelPath, 'model_quantized.onnx'), buf) 53 | } 54 | if (getMemories().length === 0) { 55 | await initMemories() 56 | } else { 57 | postMsgToMainWindow('progress ' + '100%') 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronAPI } from '@electron-toolkit/preload' 2 | import { api as Api } from './index' 3 | declare global { 4 | interface Window { 5 | electron: ElectronAPI 6 | api: typeof Api 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Gomoon 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/AlimamaFangYuanTiVF-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardAEI/Gomoon/39885fb2f66e1b6f848213e4adab7464e9efee08/src/renderer/src/assets/fonts/AlimamaFangYuanTiVF-Thin.woff2 -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/MiSans-Normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardAEI/Gomoon/39885fb2f66e1b6f848213e4adab7464e9efee08/src/renderer/src/assets/fonts/MiSans-Normal.woff2 -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/AnswerIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function AnswerIcon(props: SvgProps) { 4 | return ( 5 | 18 | 22 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/BrowserIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function BrowserIcon(props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/CapsuleIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 14 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/ChatFullIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function ChatFullIcon(props: SvgProps) { 4 | return ( 5 | 18 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/ChatIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function ChatIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/CollectionIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function CollectionIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/EscIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 6 | 15 | 24 | ESC 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/InuseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function InuseIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/LinkIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/NewChatIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 24 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/SendIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 15 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/SpeechIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/StarIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/TrashIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from './type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/ClearIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 15 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/CopyIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function CopyIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/CrossMark.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/CrossMarkRound.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/DownloadIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/Duplicate.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 22 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/EditIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function EditIcon(props: SvgProps) { 4 | return ( 5 | 15 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/EmptyIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/HistoryIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function HistoryIcon(props: SvgProps) { 4 | return ( 5 | 16 | 20 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/MoreHIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function MoreHIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/MoreIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/MoreVIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function MoreVIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/PauseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function PauseIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/PlayingIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 15 | 20 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/Plus.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/QuestionMarkIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | export default function QuestionMarkIcon(props: SvgProps) { 3 | return ( 4 | 14 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/RefreshIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/RetryIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function RetryIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/ReturnIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function ReturnIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/SaveIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function SaveIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/SettingIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | export default function SettingIcon(props: SvgProps) { 3 | return ( 4 | 17 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/ShareIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/StickTopIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function StickTopIcon(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/Toast/ErrorIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/Toast/SuccessIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/Toast/WarningIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/UploadIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 15 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/WithdrawalICon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function WithdrawalIcon(props: SvgProps) { 4 | return ( 5 | 15 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/arrow/DownwardArrow.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function DownwardArrow(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/arrow/LeftArrow.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/arrow/RightArrow.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/arrow/UpwardArrow.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function UpwardArrow(props: SvgProps) { 4 | return ( 5 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/win/ExitFullScreenIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/win/FullScreenIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/win/MinimizeIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 22 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/base/win/WinCrossIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 22 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/file/baseFileIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 16 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/models/ClaudeIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function ClaudeIcon(props: SvgProps) { 4 | return ( 5 | 14 | 15 | 16 | 22 | 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/models/CustomIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 15 | 16 | 21 | 26 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/models/DeepSeekIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function DeepSeekIcon(props: SvgProps) { 4 | return ( 5 | 14 | 15 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/models/GeminiIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 13 | 14 | 19 | 20 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/models/LlamaIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function LlamaIcon(props: SvgProps) { 4 | return ( 5 | 14 | 15 | 16 | 25 | 26 | 27 | 28 | 29 | 30 | 39 | 40 | 41 | 42 | 43 | 48 | 53 | 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/models/OllamaIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgProps } from '../type' 2 | 3 | export default function (props: SvgProps) { 4 | return ( 5 | 14 | 15 | 20 | 40 | 46 | 51 | 55 | 59 | 60 | 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon/type.tsx: -------------------------------------------------------------------------------- 1 | export interface SvgProps { 2 | width?: number 3 | height?: number 4 | fill?: string 5 | viewBox?: string 6 | class?: string 7 | onClick?: ( 8 | e: MouseEvent & { 9 | currentTarget: SVGSVGElement 10 | target: Element 11 | } 12 | ) => void 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/src/components/Capsule/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from '@solidjs/router' 2 | import { memoCapsule } from '@renderer/store/input' 3 | import { Show } from 'solid-js' 4 | import { getCurrentMemo } from '@renderer/store/memo' 5 | import { pageData } from '@renderer/store/user' 6 | import PlayingIcon from '@renderer/assets/icon/base/PlayingIcon' 7 | import CrossMarkRound from '@renderer/assets/icon/base/CrossMarkRound' 8 | import { event } from '@renderer/lib/util' 9 | 10 | import CapitalIcon from '../ui/CapitalIcon' 11 | import ModelSelect from '../MainSelections/ModelSelect' 12 | import BotIcon from '../ui/BotIcon' 13 | 14 | export default function Capsule(props: { type: 'chat' | 'ans'; botName: string; seed?: string }) { 15 | const nav = useNavigate() 16 | return ( 17 |
18 |
{ 20 | nav('/assistants?type=' + props.type) 21 | }} 22 | class="mx-[-2px] my-[-2px] flex cursor-pointer items-center gap-1 rounded-xl rounded-r-none border-2 border-solid border-transparent px-[6px] py-[3px] text-text2 duration-200 hover:border-active hover:text-text1" 23 | > 24 | {/* */} 25 | 26 | {props.botName} 27 |
28 | 29 |
{ 31 | nav('/memories?type=' + props.type) 32 | }} 33 | class="my-[-2px] ml-[-2px] flex h-7 cursor-pointer items-center gap-1 border-2 border-solid border-transparent px-1 text-text2 duration-200 hover:border-active hover:text-text1" 34 | > 35 | 36 | 37 | {getCurrentMemo().name} 38 | 39 |
40 |
41 |
42 | 43 |
44 | 45 |
46 | 47 | { 51 | event.emit('stopSpeak') 52 | }} 53 | class="absolute -left-[1px] text-active opacity-0 group-hover/playing:opacity-100" 54 | /> 55 |
56 |
57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/renderer/src/components/MainInput/Context.tsx: -------------------------------------------------------------------------------- 1 | export type ContextAction = 2 | | 'copy' 3 | | 'paste' 4 | | 'select-all' 5 | | 'cut' 6 | | 'clear' 7 | | 'new-chat' 8 | | 'switch' 9 | 10 | export default function ContextContainer(props: { 11 | onClick: (action: ContextAction) => void 12 | type: 'ai' | 'human' | 'ans' | 'question' 13 | generating: boolean 14 | selected: boolean 15 | }) { 16 | return ( 17 |
18 |
{ 21 | console.log('click') 22 | props.selected && props.onClick?.('copy') 23 | }} 24 | > 25 | 复制 26 |
27 |
{ 30 | props.selected && props.onClick?.('cut') 31 | }} 32 | > 33 | 剪切 34 |
35 |
{ 38 | props.onClick?.('paste') 39 | }} 40 | > 41 | 粘贴 42 |
43 |
{ 46 | props.onClick?.('select-all') 47 | }} 48 | > 49 | 全选内容 50 |
51 |
{ 54 | props.onClick?.('clear') 55 | }} 56 | > 57 | 清空输入框 58 |
59 |
60 |
{ 63 | !props.generating && props.onClick?.('new-chat') 64 | }} 65 | > 66 | 开启一段新对话 67 |
68 |
{ 71 | props.onClick?.('switch') 72 | }} 73 | > 74 | {props.type === 'ans' || props.type === 'question' ? '切换到连续对话' : '切换到问答'} 75 |
76 |
77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /src/renderer/src/components/Message/GlobalSearch.tsx: -------------------------------------------------------------------------------- 1 | import CrossMarkRound from '@renderer/assets/icon/base/CrossMarkRound' 2 | import { event } from '@renderer/lib/util' 3 | import { createSignal, onCleanup, onMount, Show } from 'solid-js' 4 | 5 | export const [findContent, setFindContent] = createSignal('') 6 | export const [showSearch, setShowSearch] = createSignal(false) 7 | export default function GlobalSearch() { 8 | onMount(() => { 9 | console.log('globalSearch') 10 | const globalSearch = () => { 11 | if (showSearch()) { 12 | setFindContent('') 13 | } else { 14 | const selection = window.getSelection() 15 | setFindContent(selection?.toString() || '') 16 | } 17 | setShowSearch(!showSearch()) 18 | } 19 | event.on('globalSearch', globalSearch) 20 | onCleanup(() => { 21 | event.off('globalSearch', globalSearch) 22 | }) 23 | }) 24 | 25 | return ( 26 | 27 |
28 | { 31 | setFindContent(e.target.value) 32 | }} 33 | placeholder="输入搜索内容" 34 | /> 35 | { 37 | setShowSearch(false) 38 | setFindContent('') 39 | }} 40 | width={28} 41 | height={28} 42 | class="cursor-pointer text-gray hover:text-active" 43 | /> 44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/src/components/Message/SpecialTypeContent.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable solid/components-return-once */ 2 | import BaseFileIcon from '@renderer/assets/icon/file/baseFileIcon' 3 | import { ContentDisplay } from '@renderer/lib/ai/parseString' 4 | import NetworkIcon from '@renderer/assets/icon/BrowserIcon' 5 | import LinkIcon from '@renderer/assets/icon/LinkIcon' 6 | import CapsuleIcon from '@renderer/assets/icon/CapsuleIcon' 7 | 8 | import Md from './Md' 9 | 10 | export default function SpecialTypeContent( 11 | meta: ContentDisplay, 12 | mdStyle: string, 13 | opt?: { 14 | needSelectBtn?: boolean 15 | } 16 | ) { 17 | if (meta.type === 'file') { 18 | return ( 19 |
window.api.openPath(meta.src)} 21 | class="flex cursor-pointer gap-1 py-[2px] text-text-dark2 duration-150 hover:text-active" 22 | > 23 | 24 | {meta.filename} 25 |
26 | ) 27 | } 28 | if (meta.type === 'url') { 29 | return ( 30 | 31 | 36 | 42 | {meta.src} 43 | 44 | 45 | ) 46 | } 47 | if (meta.type === 'search') { 48 | return ( 49 |
50 | 51 | 52 |
53 | ) 54 | } 55 | if (meta.type === 'memo') { 56 | return ( 57 |
58 | 59 | 64 |
65 | ) 66 | } 67 | if (meta.type === 'image') { 68 | return ( 69 |
window.api.openPath(meta.src)}> 70 | 71 |
72 | ) 73 | } 74 | return null 75 | } 76 | -------------------------------------------------------------------------------- /src/renderer/src/components/ScrollBox.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, onCleanup, type JSXElement } from 'solid-js' 2 | 3 | export default function ScrollBox(props: { children: JSXElement }) { 4 | const [needScroll, setNeedScroll] = createSignal(false) 5 | return ( 6 |
{ 8 | setTimeout(() => { 9 | dom.classList.add('scrollbar-translucent') 10 | dom.classList.add('scrollbar-transparent') 11 | }) 12 | let time: NodeJS.Timeout | null = null 13 | dom.onscrollend = () => { 14 | time = setTimeout(() => { 15 | dom.classList.add('scrollbar-translucent') 16 | setTimeout(() => { 17 | dom.classList.add('scrollbar-transparent') 18 | }, 1000) 19 | }, 2000) 20 | } 21 | dom.onscroll = () => { 22 | dom.classList.remove('scrollbar-translucent') 23 | dom.classList.remove('scrollbar-transparent') 24 | if (time) { 25 | clearTimeout(time) 26 | } 27 | } 28 | const children = dom.children[0] 29 | const resizeObserver = new ResizeObserver((entries) => { 30 | for (const entry of entries) { 31 | if (entry.contentRect.height > dom.clientHeight) { 32 | setNeedScroll(true) 33 | } 34 | } 35 | }) 36 | resizeObserver.observe(children) 37 | onCleanup(() => resizeObserver.disconnect()) 38 | }} 39 | class={'scrollbar-show h-full ' + (needScroll() ? 'w-[calc(100%+0.45rem)]' : 'w-full')} 40 | > 41 |
{props.children}
42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/BotIcon.tsx: -------------------------------------------------------------------------------- 1 | import { createAvatar } from '@dicebear/core' 2 | import { botttsNeutral } from '@dicebear/collection' 3 | import { createMemo } from 'solid-js' 4 | 5 | export default function BotIcon(props: { size: number; seed: string }) { 6 | const avatar = createMemo(() => { 7 | const avatar = createAvatar(botttsNeutral, { 8 | seed: props.seed, 9 | size: props.size, 10 | radius: 50, 11 | backgroundType: ['gradientLinear', 'solid'] 12 | }) 13 | return avatar.toDataUri() 14 | }) 15 | 16 | // eslint-disable-next-line solid/no-innerhtml 17 | return Bot icon 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX, JSXElement } from 'solid-js' 2 | 3 | export default function Button( 4 | props: { children: JSXElement } & JSX.HTMLAttributes 5 | ) { 6 | return ( 7 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/CapitalIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as tooltip from '@zag-js/tooltip' 2 | import { normalizeProps, useMachine } from '@zag-js/solid' 3 | import { createMemo, createUniqueId, Show } from 'solid-js' 4 | 5 | export default function CapitalIcon(props: { 6 | size?: number 7 | content: string 8 | bg?: 'bg-gray' | 'bg-active-gradient' | 'bg-green-gradient' 9 | hiddenTiptop?: boolean 10 | }) { 11 | const [state, send] = useMachine( 12 | tooltip.machine({ id: createUniqueId(), openDelay: 200, closeDelay: 300 }) 13 | ) 14 | 15 | const api = createMemo(() => tooltip.connect(state, send, normalizeProps)) 16 | const firstChat = props.content.charAt(0) 17 | 18 | return ( 19 |
20 | 41 | 42 |
43 |
44 | {props.content} 45 |
46 |
47 |
48 |
49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/Card.tsx: -------------------------------------------------------------------------------- 1 | import { JSXElement } from 'solid-js' 2 | 3 | export default function Card(props: { title: string; children: JSXElement; noPadding?: boolean }) { 4 | return ( 5 |
6 |
{props.title}
7 |
{props.children}
8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/CheckItem.tsx: -------------------------------------------------------------------------------- 1 | import * as checkbox from '@zag-js/checkbox' 2 | import { normalizeProps, useMachine } from '@zag-js/solid' 3 | import { createMemo, createUniqueId } from 'solid-js' 4 | 5 | import QuestionMention from './QuestionMention' 6 | 7 | export function CheckItem(props: { 8 | label: string 9 | hint?: string 10 | checked: boolean 11 | onCheckedChange: (checked: boolean) => void 12 | }) { 13 | const [state, send] = useMachine( 14 | checkbox.machine({ 15 | id: createUniqueId(), 16 | checked: props.checked, 17 | onCheckedChange: (v) => props.onCheckedChange(v.checked as boolean) 18 | }) 19 | ) 20 | const api = createMemo(() => checkbox.connect(state, send, normalizeProps)) 21 | 22 | return ( 23 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/Collapse.tsx: -------------------------------------------------------------------------------- 1 | import DownwardArrow from '@renderer/assets/icon/base/arrow/DownwardArrow' 2 | import UpwardArrow from '@renderer/assets/icon/base/arrow/UpwardArrow' 3 | import { JSX, JSXElement, Show, createSignal } from 'solid-js' 4 | export default function Collapse(props: { title: string | JSX.Element; children: JSXElement }) { 5 | const [collapsed, setCollapsed] = createSignal(true) 6 | 7 | return ( 8 |
9 |
setCollapsed(!collapsed())} 11 | class={` ${ 12 | collapsed() ? 'hover:bg-dark-con' : '' 13 | } group/expand my-1 flex cursor-pointer justify-between rounded-lg bg-dark px-2 py-1 text-sm duration-200`} 14 | > 15 |
{props.title}
16 | } 19 | > 20 | 21 | 22 |
23 | 24 |
{props.children}
25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/DoubleConfirm.tsx: -------------------------------------------------------------------------------- 1 | import { JSX, Show, createSignal, onCleanup } from 'solid-js' 2 | 3 | export default function (props: { 4 | label: string 5 | children: JSX.Element 6 | position?: string 7 | onConfirm: () => void 8 | onCancel?: () => void 9 | preConfirm?: () => boolean 10 | popup?: boolean 11 | }) { 12 | const [show, setShow] = createSignal(false) 13 | const animation = () => { 14 | return props.popup ? 'animate-popup' : 'animate-dropdown' 15 | } 16 | return ( 17 |
e.stopPropagation()} 20 | ref={(el) => { 21 | const fn = (e) => { 22 | if (e.currentTarget && el.contains(e.currentTarget)) { 23 | return 24 | } 25 | setShow(false) 26 | } 27 | document.addEventListener('click', fn) 28 | onCleanup(() => { 29 | document.removeEventListener('click', fn) 30 | }) 31 | }} 32 | > 33 |
{ 35 | e.stopPropagation() 36 | if (props.preConfirm && !props.preConfirm()) { 37 | setShow(false) 38 | return 39 | } 40 | setShow(true) 41 | }} 42 | > 43 | {props.children} 44 |
45 | 46 |
52 |
{props.label}
53 |
54 | 64 | 74 |
75 |
76 |
77 |
78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/DynamicLoading.tsx: -------------------------------------------------------------------------------- 1 | import { Show, createContext, createSignal, useContext } from 'solid-js' 2 | import type { Accessor, Setter, JSX } from 'solid-js' 3 | 4 | import Loading from './Loading' 5 | interface LoadingConf { 6 | msg: string 7 | show: boolean 8 | } 9 | 10 | const UIContext = createContext<{ 11 | loading: Accessor 12 | setLoading: Setter 13 | }>() 14 | 15 | function LoadingWrap() { 16 | return ( 17 | 18 |
19 |
20 | 21 | {useContext(UIContext)?.loading().msg} 22 |
23 |
24 |
25 | ) 26 | } 27 | 28 | export function LoadingProvider(props: { children: JSX.Element }) { 29 | const [loading, setLoading] = createSignal({ 30 | show: false, 31 | msg: '' 32 | }) 33 | 34 | return ( 35 | 41 | {props.children} 42 | 43 | 44 | ) 45 | } 46 | 47 | export function useLoading() { 48 | const { setLoading: setLoading } = useContext(UIContext)! 49 | return { 50 | show: (msg: string) => { 51 | setLoading({ 52 | show: true, 53 | msg 54 | }) 55 | }, 56 | hide: () => { 57 | setLoading({ 58 | show: false, 59 | msg: '' 60 | }) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/EditInput.tsx: -------------------------------------------------------------------------------- 1 | import SaveIcon from '@renderer/assets/icon/base/SaveIcon' 2 | import { Show, createSignal } from 'solid-js' 3 | 4 | export default function EditInput( 5 | props: { 6 | label?: string 7 | value?: string 8 | spellcheck?: boolean 9 | onSave: (value: string) => void 10 | optional?: boolean 11 | type?: 'text' | 'number' 12 | } = { 13 | onSave: () => {}, 14 | type: 'text' 15 | } 16 | ) { 17 | // eslint-disable-next-line solid/reactivity 18 | const [value, setValue] = createSignal(props.value || '') 19 | const [isEditing, setIsEditing] = createSignal(false) 20 | let inputRef: HTMLInputElement | undefined 21 | const onSave = () => { 22 | props.onSave(value()) 23 | setIsEditing(false) 24 | } 25 | 26 | return ( 27 |
28 | 29 |
{props.label}
30 |
31 | { 37 | setIsEditing(true) 38 | inputRef?.focus() 39 | }} 40 | > 41 | 42 | {props.value || '填写您的 ' + props.label + (props.optional ? '(非必填)' : '')} 43 | 44 |
45 | } 46 | > 47 |
48 | setValue((e.target as HTMLInputElement).value)} 55 | onBlur={onSave} 56 | /> 57 | 58 | 64 | 65 |
66 | 67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/Expand.tsx: -------------------------------------------------------------------------------- 1 | import { type JSX, type JSXElement, Show, createSignal } from 'solid-js' 2 | 3 | function ExpandWrapper(props: { children: JSXElement; onChange: () => void }) { 4 | return ( 5 |
props.onChange()} 8 | > 9 | {props.children} 10 |
11 | ) 12 | } 13 | 14 | export default function (props: { 15 | title?: string | JSX.Element 16 | foldTitle?: string | JSX.Element 17 | children: JSXElement 18 | }) { 19 | const [expanded, setExpanded] = createSignal(false) 20 | return ( 21 |
22 | setExpanded(true)}>{props.title || '展开'} 26 | } 27 | > 28 | {props.children} 29 | setExpanded(false)}> 30 | {props.foldTitle || '收起'} 31 | 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/FilePicker.tsx: -------------------------------------------------------------------------------- 1 | export default function (props: { onChange: (val: string) => void; path: string }) { 2 | return ( 3 |
4 | 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/Loading.tsx: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | export default function Loading() { 3 | return ( 4 | 11 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/QuestionMention.tsx: -------------------------------------------------------------------------------- 1 | import QuestionMarkIcon from '@renderer/assets/icon/base/QuestionMarkIcon' 2 | import * as tooltip from '@zag-js/tooltip' 3 | import { normalizeProps, useMachine } from '@zag-js/solid' 4 | import { createMemo, createUniqueId, type JSX, Show } from 'solid-js' 5 | 6 | export default function QuestionMention(props: { 7 | size?: number 8 | color?: string 9 | content: string | JSX.Element 10 | fill?: string 11 | }) { 12 | const [state, send] = useMachine( 13 | tooltip.machine({ 14 | closeOnPointerDown: false, 15 | interactive: true, 16 | id: createUniqueId(), 17 | openDelay: 200, 18 | closeDelay: 300, 19 | positioning: { 20 | placement: 'top' 21 | } 22 | }) 23 | ) 24 | 25 | const api = createMemo(() => tooltip.connect(state, send, normalizeProps)) 26 | 27 | return ( 28 |
29 | 40 | 41 |
42 |
49 | {props.content} 50 |
51 |
52 |
53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/Search.tsx: -------------------------------------------------------------------------------- 1 | import SearchIcon from '@renderer/assets/icon/base/SearchIcon' 2 | import { createSignal } from 'solid-js' 3 | 4 | export function Search(props: { placeholder: string; onChange: (value: string) => void }) { 5 | const [composition, setComposition] = createSignal(false) 6 | return ( 7 |
8 |
9 | 10 |
11 | { 13 | el.addEventListener('compositionstart', () => { 14 | setComposition(true) 15 | }) 16 | el.addEventListener('compositionend', () => { 17 | setComposition(false) 18 | }) 19 | }} 20 | class="h-8 rounded-lg pl-9" 21 | type="text" 22 | placeholder={props.placeholder} 23 | onInput={(e) => { 24 | setTimeout(() => { 25 | if (composition()) return 26 | props.onChange(e.target.value) 27 | }) 28 | }} 29 | /> 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/SegmentedControl.tsx: -------------------------------------------------------------------------------- 1 | import * as radio from '@zag-js/radio-group' 2 | import { normalizeProps, useMachine } from '@zag-js/solid' 3 | import { Index, JSXElement, createMemo, createUniqueId } from 'solid-js' 4 | 5 | const items = [ 6 | { label: 'React', value: 'react' }, 7 | { label: 'Angular', value: 'ng' }, 8 | { label: 'Vue', value: 'vue' }, 9 | { label: 'Svelte', value: 'svelte' } 10 | ] 11 | 12 | export function SegmentedControl(props: { 13 | options: { 14 | label: string | JSXElement 15 | value: string 16 | }[] 17 | defaultValue: string 18 | onCheckedChange: (checked: string) => void 19 | }) { 20 | const [state, send] = useMachine( 21 | radio.machine({ 22 | id: createUniqueId(), 23 | value: props.defaultValue, 24 | onValueChange(details) { 25 | props.onCheckedChange(details.value) 26 | } 27 | }) 28 | ) 29 | 30 | const api = createMemo(() => radio.connect(state, send, normalizeProps)) 31 | 32 | return ( 33 |
34 |
38 | 39 | {(item) => ( 40 | 49 | )} 50 | 51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/Select.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type Component, 3 | createMemo, 4 | createSignal, 5 | For, 6 | type JSXElement, 7 | Show, 8 | onCleanup 9 | } from 'solid-js' 10 | 11 | // 选项类型,可以是字符串或 JSXElement 元素 12 | type OptionType = { 13 | value: string 14 | label: JSXElement | string 15 | } 16 | 17 | // Select 组件的 Props 18 | type SelectProps = { 19 | defaultValue: string 20 | options: OptionType[] 21 | onSelect: (value: string) => void 22 | } 23 | 24 | /** 25 | * @returns 26 | * @description defaultValue 默认值, options: { 27 | value: string 28 | label: JSXElement | string 29 | } 30 | onSelect: (value: string) => void 31 | */ 32 | const Select: Component = (props) => { 33 | // 使用 createSignal 来管理下拉菜单的显示状态 34 | const [isOpen, setIsOpen] = createSignal(false) 35 | // eslint-disable-next-line solid/reactivity 36 | const options = props.options.map((option) => ({ 37 | value: option.value, 38 | get label() { 39 | return option.label 40 | } 41 | })) 42 | // 使用 createSignal 来管理选中的值 43 | const [selectedValue, setSelectedValue] = createSignal(props.defaultValue) 44 | const label = createMemo(() => { 45 | const cmp = options.find((opt) => opt.value === selectedValue())?.label 46 | return ( 47 |
48 |
{cmp ?? 'default value'}
49 |
50 | ) 51 | }) 52 | // 选择选项的处理函数 53 | const handleSelect = (option: OptionType) => { 54 | setSelectedValue(option.value) 55 | setIsOpen(false) 56 | props.onSelect(option.value) 57 | } 58 | 59 | return ( 60 |
61 |
{ 64 | const fn = (e) => { 65 | if (e.target && el.contains(e.target)) { 66 | setIsOpen((i) => !i) 67 | e.stopPropagation() 68 | return 69 | } 70 | setIsOpen(false) 71 | } 72 | document.addEventListener('click', fn) 73 | onCleanup(() => { 74 | document.removeEventListener('click', fn) 75 | }) 76 | }} 77 | > 78 | {label()} 79 |
80 | 81 |
82 |
83 | 84 | {(option) => ( 85 |
handleSelect(option)}> 86 |
{option.label}
87 |
88 | )} 89 |
90 |
91 |
92 |
93 |
94 | ) 95 | } 96 | 97 | export default Select 98 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/Slider.tsx: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash' 2 | import { Show, createEffect, createSignal } from 'solid-js' 3 | 4 | export default function (props: { 5 | onChange: (value: number) => void 6 | value?: number 7 | min?: number 8 | max?: number 9 | percentage?: boolean 10 | }) { 11 | let container: HTMLDivElement | undefined 12 | let range: HTMLInputElement | undefined 13 | const [value, setValue] = createSignal( 14 | props.value !== undefined ? (props.percentage ? props.value * 100 : props.value) : 70 15 | ) 16 | createEffect(() => { 17 | const containerWidth = container?.getBoundingClientRect().width 18 | range!.style.width = `${ 19 | (containerWidth! - 34) * 20 | ((value() - (props.min || 0)) / ((props.max || 100) - (props.min || 0))) 21 | }px` 22 | }) 23 | const change = debounce((num) => props.onChange(num), 300) 24 | const updateValue = (e: unknown) => { 25 | const value = ((e as { target: unknown }).target as HTMLInputElement).value 26 | setValue(Number(value)) 27 | const num = props.percentage ? parseFloat((Number(value) / 100).toFixed(2)) : Number(value) 28 | change(num) 29 | } 30 | return ( 31 |
32 |
33 | 41 | 42 | { 43 | 44 | {value() / 100} 45 | 46 | } 47 | 48 |
49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/SwitchItem.tsx: -------------------------------------------------------------------------------- 1 | import * as zagSwitch from '@zag-js/switch' 2 | import { normalizeProps, useMachine } from '@zag-js/solid' 3 | import { type JSXElement, createMemo, createUniqueId } from 'solid-js' 4 | 5 | import QuestionMention from './QuestionMention' 6 | 7 | export default function Switch(props: { 8 | label: string | JSXElement 9 | hint?: string 10 | size?: 'sm' | 'md' 11 | checked: boolean 12 | onCheckedChange: (checked: boolean) => void 13 | }) { 14 | const [state, send] = useMachine( 15 | zagSwitch.machine({ 16 | id: createUniqueId(), 17 | // eslint-disable-next-line solid/reactivity 18 | checked: props.checked, 19 | onCheckedChange: (v) => props.onCheckedChange(v.checked as boolean) 20 | }) 21 | ) 22 | 23 | const api = createMemo(() => zagSwitch.connect(state, send, normalizeProps)) 24 | 25 | return ( 26 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/ToolTip.tsx: -------------------------------------------------------------------------------- 1 | import { type JSXElement } from 'solid-js' 2 | import { PositioningOptions } from '@zag-js/popper' 3 | import * as tooltip from '@zag-js/tooltip' 4 | import { normalizeProps, useMachine } from '@zag-js/solid' 5 | import { createMemo, createUniqueId, Show } from 'solid-js' 6 | 7 | export default function (props: { 8 | size?: number 9 | color?: string 10 | label: string | JSXElement 11 | content: string | JSXElement 12 | fill?: string 13 | position?: PositioningOptions 14 | }) { 15 | const [state, send] = useMachine( 16 | tooltip.machine({ 17 | id: createUniqueId(), 18 | interactive: true, 19 | openDelay: 200, 20 | closeDelay: 300, 21 | // eslint-disable-next-line solid/reactivity 22 | positioning: props.position || { 23 | placement: 'top' 24 | } 25 | }) 26 | ) 27 | 28 | const api = createMemo(() => tooltip.connect(state, send, normalizeProps)) 29 | return ( 30 |
31 | 37 | 38 |
39 |
43 | {props.content} 44 |
45 |
46 |
47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/compWithTip.tsx: -------------------------------------------------------------------------------- 1 | import { type JSXElement, Show, createSignal } from 'solid-js' 2 | 3 | // FEAT: 让点击可以有反馈 4 | export const compWithTip = ( 5 | fn: (tip: (status: 'success' | 'fail', label: string) => void) => JSXElement, 6 | position?: 'right' | 'left' 7 | ): JSXElement => { 8 | const [tipModal, setTipModal] = createSignal<{ 9 | status: '' | 'success' | 'fail' 10 | label: string 11 | }>({ 12 | status: '', 13 | label: '' 14 | }) 15 | const tip = (status: 'success' | 'fail', label: string) => { 16 | setTipModal({ 17 | status, 18 | label 19 | }) 20 | setTimeout(() => { 21 | setTipModal({ 22 | status: '', 23 | label: '' 24 | }) 25 | }, 1000) 26 | } 27 | const Comp = fn(tip) 28 | return ( 29 |
34 | 35 | {tipModal().status === 'success' && ( 36 |
37 | {tipModal().label || '成功!'} 38 |
39 | )} 40 | {tipModal().status === 'fail' && ( 41 |
42 | {tipModal().label || '失败!'} 43 |
44 | )} 45 |
46 | {Comp} 47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/style.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | animation: rotator 1.4s linear infinite; 3 | } 4 | 5 | @keyframes rotator { 6 | 0% { 7 | transform: rotate(0deg); 8 | } 9 | 100% { 10 | transform: rotate(270deg); 11 | } 12 | } 13 | 14 | .path { 15 | stroke-dasharray: 187; 16 | stroke-dashoffset: 0; 17 | transform-origin: center; 18 | animation: 19 | dash 1.4s ease-in-out infinite, 20 | colors 5.6s ease-in-out infinite; 21 | } 22 | 23 | @keyframes colors { 24 | 0% { 25 | stroke: #4285f4; 26 | } 27 | 25% { 28 | stroke: #de3e35; 29 | } 30 | 50% { 31 | stroke: #f7c223; 32 | } 33 | 75% { 34 | stroke: #1b9a59; 35 | } 36 | 100% { 37 | stroke: #4285f4; 38 | } 39 | } 40 | 41 | @keyframes dash { 42 | 0% { 43 | stroke-dashoffset: 187; 44 | } 45 | 50% { 46 | stroke-dashoffset: 46; 47 | transform: rotate(135deg); 48 | } 49 | 100% { 50 | stroke-dashoffset: 187; 51 | transform: rotate(450deg); 52 | } 53 | } 54 | 55 | /* Range Slider */ 56 | .range-slider { 57 | width: 100%; 58 | } 59 | 60 | .range-slider__range { 61 | -webkit-appearance: none; 62 | height: 10px; 63 | border-radius: 5px; 64 | background: #d7dcdf; 65 | outline: none; 66 | padding: 0; 67 | margin: 0; 68 | } 69 | 70 | .range-slider__range::-webkit-slider-thumb { 71 | @apply bg-white; 72 | position: relative; 73 | appearance: none; 74 | width: 16px; 75 | height: 16px; 76 | border-radius: 50%; 77 | /* background: #2c3e50; */ 78 | cursor: pointer; 79 | transition: background 0.15s ease-in-out; 80 | z-index: 1; 81 | } 82 | 83 | .range-slider__range::-webkit-slider-thumb:hover { 84 | @apply bg-active; 85 | } 86 | 87 | .range-slider__range:active::-webkit-slider-thumb { 88 | @apply bg-active; 89 | } 90 | 91 | /* .range-slider__range:focus::-webkit-slider-thumb { 92 | box-shadow: 93 | 0 0 0 2px #fff, 94 | 0 0 0 4px #1abc9c; 95 | } */ 96 | -------------------------------------------------------------------------------- /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { ElectronAPI } from '@electron-toolkit/preload' 4 | import { api as Api } from '../../preload/index' 5 | declare global { 6 | interface Window { 7 | electron: ElectronAPI 8 | api: typeof Api 9 | } 10 | type Tesseract = typeof import('tesseract.js') 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/src/lib/ai/file/index.ts: -------------------------------------------------------------------------------- 1 | import { FileLoaderRes } from 'src/main/lib/ai/fileLoader' 2 | 3 | export async function parseFile(file: File): Promise< 4 | { 5 | suc: boolean 6 | length: number 7 | } & FileLoaderRes 8 | > { 9 | // .doc 10 | if (file.type === 'application/msword') { 11 | return { 12 | suc: false, 13 | content: '请将 doc 文件转换成 docx 格式后重新发送', 14 | length: 0, 15 | src: '', 16 | filename: '', 17 | type: 'file' 18 | } 19 | } 20 | let fileLoader: FileLoaderRes 21 | if (!file.path) { 22 | fileLoader = await new Promise((resolve, reject) => { 23 | const reader = new FileReader() 24 | reader.onload = async function (event) { 25 | resolve( 26 | await window.api.parseFile([ 27 | { 28 | path: file.name, 29 | type: file.type || 'text/plain', 30 | data: event.target?.result as string 31 | } 32 | ]) 33 | ) 34 | } 35 | reader.onerror = function (event) { 36 | reject(event.target?.error) 37 | } 38 | reader.readAsDataURL(file) // 读取文件内容为 Data URL 39 | }) 40 | } else { 41 | fileLoader = await window.api.parseFile([ 42 | { 43 | path: file.path, 44 | type: file.type || 'text/plain' 45 | } 46 | ]) 47 | } 48 | try { 49 | let content = '' 50 | if (fileLoader.type === 'file') { 51 | const type = fileLoader.filename.split('.').pop() ?? '文本' 52 | content = 53 | `这是一个${type}文件的文本内容:\n` + 54 | fileLoader.content + 55 | '' 56 | } else if (fileLoader.type === 'image') { 57 | content = 58 | `` + 59 | fileLoader.content + 60 | '' 61 | } 62 | console.log(fileLoader.content) 63 | return { 64 | suc: true, 65 | content, 66 | length: fileLoader.content.length, 67 | src: fileLoader.src, 68 | filename: fileLoader.filename, 69 | type: fileLoader.type 70 | } 71 | } catch (e) { 72 | return { 73 | suc: false, 74 | content: '解析失败' + (e as Error).message, 75 | length: 0, 76 | src: '', 77 | filename: '', 78 | type: fileLoader.type 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/renderer/src/lib/ai/langchain/models.ts: -------------------------------------------------------------------------------- 1 | import { defaultModels } from '@lib/langchain' 2 | import { loadLMMap } from '@lib/langchain' 3 | 4 | import { event } from '../../util' 5 | 6 | export const models = { 7 | ...(await loadLMMap(defaultModels())) 8 | } 9 | 10 | event.on('updateModels', async (model) => { 11 | const loadedModels = await loadLMMap(model) 12 | for (const key in models) { 13 | models[key] = loadedModels[key] 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/renderer/src/lib/ai/memo/index.ts: -------------------------------------------------------------------------------- 1 | import { MemoResult } from 'src/main/models/model' 2 | 3 | export function processMemo(question: string, memories: MemoResult[]) { 4 | return `${question}${question} 5 | 这是一些可能和上述问题有关的参考信息:${memories 6 | .map((m) => m.content) 7 | .join('')}` 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/src/lib/ai/ocr.ts: -------------------------------------------------------------------------------- 1 | let worker: Tesseract.Worker | undefined 2 | export const init = async () => { 3 | worker = await Tesseract.createWorker('eng+chi_sim', undefined, { 4 | logger: (_m) => {} 5 | }) 6 | } 7 | 8 | export const recognizeText = async ( 9 | img: File, 10 | logger: (m: Partial) => void 11 | ) => { 12 | if (worker) { 13 | logger({ 14 | status: '正在识别图片中的文字' 15 | }) 16 | const result = await worker?.recognize(img) 17 | return result.data.text 18 | } else { 19 | logger({ 20 | status: '加载识别引擎中' 21 | }) 22 | await init() 23 | const result = await worker!.recognize(img) 24 | return result.data.text 25 | } 26 | } 27 | 28 | export const terminate = async () => { 29 | await worker?.terminate() 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/src/lib/ai/search/index.ts: -------------------------------------------------------------------------------- 1 | import { nonStreamingAssistant } from '../langchain' 2 | import { parsePageToString } from '../url' 3 | 4 | async function fetchBaiduResults(keyword) { 5 | const searchUrl = `https://www.baidu.com/s?wd=${encodeURIComponent(keyword)}&pn=1&rn=5&tn=json` 6 | const data = await fetch(searchUrl).then((res) => res.json()) 7 | if (!data.feed?.entry?.length) { 8 | return [] 9 | } 10 | const links: { 11 | summary: string 12 | link: string 13 | }[] = [] 14 | 15 | links.push( 16 | ...data.feed.entry.map((item) => ({ 17 | summary: item.abs, 18 | link: item.url 19 | })) 20 | ) 21 | 22 | return links.filter(({ summary }) => summary) 23 | } 24 | 25 | function processQuery(content: string) { 26 | const reg = /<(.+?)>/ 27 | if (content.match(reg)) { 28 | return { 29 | suc: true, 30 | query: content.match(reg)![0].replace(/<(.+?)>$/, '$1') 31 | } 32 | } 33 | return { 34 | suc: false, 35 | query: '' 36 | } 37 | } 38 | export async function searchByBaidu(question: string, logger: (content: string) => void) { 39 | const prompt = `请根据『 ${question} 』这个问题给我一个或多个使用于搜索引擎的关键词,并使用尖括号<>包裹。除了关键词和尖括号外不要添加任何其他内容。例如我问你今天北京的天气,你可以回复:<天气 北京>` 40 | let times = 3 41 | logger('创建查询中...') 42 | let processedContent = { 43 | suc: false, 44 | query: '' 45 | } 46 | while (times--) { 47 | const res = await nonStreamingAssistant(prompt) 48 | processedContent = processQuery(res.content as string) 49 | if (processedContent.suc) { 50 | break 51 | } 52 | logger('查询创建失败,重新创建中...') 53 | } 54 | if (!processedContent.suc) { 55 | throw new Error('查询创建失败,请重新提问') 56 | } 57 | logger('收集链接中...') 58 | const links = await fetchBaiduResults(processedContent.query) 59 | const valuableLinks: string[] = [] 60 | const linkNum = links.length 61 | while (links.length) { 62 | if (valuableLinks.length >= 2) { 63 | break 64 | } 65 | const link = links.pop() 66 | const prompt2 = `我将给你一段网页简介,请预测该网页是否可能和问题 ${question} 有关,如果相关请回复yes,否则请回复no。不要添加任何其他内容。 67 | 简介内容: 68 | ${link!.summary!.slice(0, 150)} 69 | ` 70 | try { 71 | const c = (await nonStreamingAssistant(prompt2)).content as string 72 | if (c.includes('yes')) { 73 | valuableLinks.push(link!.link) 74 | } 75 | } finally { 76 | logger( 77 | `收集链接中(${valuableLinks.length}/2),${ 78 | linkNum - links.length - valuableLinks.length 79 | }个无效链接` 80 | ) 81 | } 82 | } 83 | if (valuableLinks.length === 0) { 84 | throw new Error('没有获取到有效的链接,请重新提问') 85 | } 86 | let content = `接下来我将会给你几个可能与\n ${question} \n这个问题有关的网页文本内容(这其中可能会包括一些标题,用户信息,备案号,相关推荐,按钮内容等。请你剔除无效信息)。请综合理解信息并直接给出最终答案,不要多余的解释。(不要说自己不能联网或获取不到实时信息等), 请在回答问题后附上参考网页地址:\n` 87 | logger(`解析链接中...`) 88 | const res = await Promise.all( 89 | valuableLinks.map(async (link) => { 90 | return { 91 | content: await parsePageToString(link), 92 | link 93 | } 94 | }) 95 | ) 96 | res.map((r, i) => { 97 | content += `\n\n\n第${i + 1}个[网页](${r.link})信息:${r.content}` 98 | }) 99 | return `${question}${content}` 100 | } 101 | -------------------------------------------------------------------------------- /src/renderer/src/lib/ai/tts.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardAEI/Gomoon/39885fb2f66e1b6f848213e4adab7464e9efee08/src/renderer/src/lib/ai/tts.ts -------------------------------------------------------------------------------- /src/renderer/src/lib/ai/url/index.ts: -------------------------------------------------------------------------------- 1 | // TODO: 优化内容获取截取逻辑(或者分片) 2 | export async function parsePageToString(url: string): Promise { 3 | // 如果 url违法 返回 url 违法 4 | try { 5 | return await window.api.parsePageToString(url) 6 | } catch (e) { 7 | return (e as Error).message 8 | } 9 | } 10 | 11 | export async function parsePageForUrl(url: string) { 12 | const content = await parsePageToString(url) 13 | return ( 14 | `这是一个网址下的文本内容,其中可能会包括一些标题,用户信息,备案号,相关推荐,按钮内容等无效信息:\n` + 15 | content + 16 | '' 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/src/lib/constant.ts: -------------------------------------------------------------------------------- 1 | function stringError(err: Error) { 2 | return `\n\n发生错误: ${err.message}` 3 | } 4 | 5 | export function ErrorDict(err: Error): string { 6 | console.log('error', err) 7 | if (err.message.includes('AbortError') || err.name.includes('AbortError')) { 8 | return ' ⏹' 9 | } else if ( 10 | err.message.includes('Request timed out.') || 11 | err.name.includes('Request timed out.') 12 | ) { 13 | return '\n\n回答超时,请检查网络后重试' 14 | } else if (err.message.includes('401') || err.message.includes('Failed to fetch')) { 15 | return `密钥或BaseURL不正确。\n请点击${ 16 | navigator.userAgent.includes('Mac') ? '右' : '左' 17 | }上角设置,进入设置页面进行设置。` 18 | } else if (err.message.includes('404')) { 19 | return `${stringError(err)}\n这通常是由于您未配置上述缺少的模型。` 20 | } else if (err.message.includes('maximum')) { 21 | return `${stringError(err)}\n这通常是由于您的总字数超过了模型的限制。` 22 | } else if (err.message.includes('Open api daily request limit reached')) { 23 | return `${stringError(err)}\n如果你是文心用户,请于[在线服务](https://console.bce.baidu.com/qianfan/ais/console/onlineService?tab=preset)开通付费。\n 文心3需要开通\`ERNIE-3.5-8K\`;文心4需要开通\`ERNIE-4.0-8K\`;文心128k需要开通\`ERNIE-Speed-128K\`。` 24 | } 25 | return `${stringError(err)}` 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/src/lib/util.ts: -------------------------------------------------------------------------------- 1 | import { Models } from 'src/lib/langchain' 2 | 3 | // 消息中心 (发布订阅) 4 | // eslint-disable-next-line @typescript-eslint/ban-types 5 | const Events = new Map>() 6 | 7 | export type Events = { 8 | reGenMsg: (id: string) => void // 告知 Chat 页面重新生成答案 9 | editUserMsg: (content: string, id: string) => void // 告知 Chat 页面修改用户的信息 10 | updateModels: (newModels: Models) => void // 告知修改模型信息 11 | stopSpeak: () => void // 告知页面停止说话 12 | globalSearch: () => void // 告知页面进行全局搜索 13 | } 14 | export const event = { 15 | on(event: T, callback: Events[T]) { 16 | if (!Events.has(event)) { 17 | Events.set(event, new Set()) 18 | } 19 | Events.get(event)!.add(callback) 20 | }, 21 | off(event: T, callback: Events[T]) { 22 | if (Events.has(event)) { 23 | Events.get(event)!.delete(callback) 24 | } 25 | }, 26 | emit(event: T, ...args: Parameters) { 27 | if (Events.has(event)) { 28 | for (const callback of Events.get(event)!) { 29 | callback(...args) 30 | } 31 | } 32 | } 33 | } 34 | 35 | export const getSystem: () => 'mac' | 'linux' | 'win' = () => { 36 | const ua = navigator.userAgent 37 | if (ua.match(/windows/i)) { 38 | return 'win' 39 | } 40 | if (ua.match(/mac/i)) { 41 | return 'mac' 42 | } 43 | return 'linux' 44 | } 45 | 46 | export const isValidUrl = (url: string) => { 47 | try { 48 | new URL(url) 49 | return true 50 | } catch (err) { 51 | return false 52 | } 53 | } 54 | 55 | export const getRandomString = (len: number) => { 56 | const str = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 57 | let result = '' 58 | for (let i = 0; i < len; i++) { 59 | result += str[Math.floor(Math.random() * str.length)] 60 | } 61 | return result 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'solid-js/web' 2 | import './assets/index.css' 3 | import { Route, HashRouter } from '@solidjs/router' 4 | 5 | import App from './App' 6 | import Chat from './pages/Chat' 7 | import Answer from './pages/Answer' 8 | import Setting from './pages/Setting' 9 | import Assistants from './pages/Assistants' 10 | import Memo from './pages/Memo' 11 | import History from './pages/History' 12 | 13 | render( 14 | () => ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ), 25 | document.getElementById('root') as HTMLElement 26 | ) 27 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Answer/SelectAssistantModel.tsx: -------------------------------------------------------------------------------- 1 | import EscIcon from '@renderer/assets/icon/EscIcon' 2 | import { assistants } from '@renderer/store/assistants' 3 | import { setSelectedAssistantForAns } from '@renderer/store/user' 4 | import { For, onCleanup, onMount } from 'solid-js' 5 | 6 | const SelectKey = { 7 | 0: ( 8 | 18 | 22 | 23 | ), 24 | 1: ( 25 |
26 | 1 27 |
28 | ), 29 | 2: ( 30 |
31 | 2 32 |
33 | ), 34 | 3: ( 35 |
36 | 3 37 |
38 | ), 39 | 4: ( 40 |
41 | 4 42 |
43 | ) 44 | } 45 | 46 | export default function (props: { onConfirm: () => void; onCancel: () => void }) { 47 | onMount(() => { 48 | async function select(e: KeyboardEvent) { 49 | const a = assistants.filter((a) => a.type === 'ans') 50 | // ctrl 或 ⌘ 键 51 | if (e.ctrlKey || e.metaKey) return 52 | if (e.code === 'Space') { 53 | await setSelectedAssistantForAns(a[0].id) 54 | props.onConfirm() 55 | } 56 | if (e.code === 'Digit1') { 57 | await setSelectedAssistantForAns(a[1].id) 58 | props.onConfirm() 59 | } 60 | if (e.code === 'Digit2') { 61 | await setSelectedAssistantForAns(a[2].id) 62 | props.onConfirm() 63 | } 64 | if (e.code === 'Digit3') { 65 | await setSelectedAssistantForAns(a[3].id) 66 | props.onConfirm() 67 | } 68 | if (e.code === 'Digit4') { 69 | await setSelectedAssistantForAns(a[4].id) 70 | props.onConfirm() 71 | } 72 | if (e.code === 'Escape') { 73 | props.onCancel() 74 | } 75 | } 76 | window.addEventListener('keydown', select) 77 | onCleanup(() => window.removeEventListener('keydown', select)) 78 | }) 79 | return ( 80 |
81 |
82 | a.type === 'ans').slice(0, 5)}> 83 | {(a, i) => ( 84 |
{ 86 | await setSelectedAssistantForAns(a.id) 87 | props.onConfirm() 88 | }} 89 | class="flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border-solid border-transparent bg-dark px-4 py-1 hover:border-active" 90 | > 91 | {a.name} 92 | {SelectKey[i()]} 93 |
94 | )} 95 |
96 |
props.onCancel()} 98 | class="flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border-solid border-transparent bg-dark px-4 py-1 hover:border-active" 99 | > 100 | 退出 (ESC) 101 | 102 |
103 |
104 |
105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /src/renderer/src/pages/History/SpecialTypeContent.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable solid/components-return-once */ 2 | import BaseFileIcon from '@renderer/assets/icon/file/baseFileIcon' 3 | import { ContentDisplay } from '@renderer/lib/ai/parseString' 4 | import NetworkIcon from '@renderer/assets/icon/BrowserIcon' 5 | import LinkIcon from '@renderer/assets/icon/LinkIcon' 6 | import CapsuleIcon from '@renderer/assets/icon/CapsuleIcon' 7 | 8 | import { decorateContent } from './utils' 9 | 10 | // 预渲染一遍,不考虑响应式 11 | export default function SpecialTypeContent( 12 | meta: ContentDisplay, 13 | max: number = 50, 14 | displayFull: boolean = false 15 | ) { 16 | if (meta.type === 'file') { 17 | return ( 18 | 19 | 24 | {decorateContent(meta.filename)} 25 | 26 | ) 27 | } 28 | if (meta.type === 'url') { 29 | return ( 30 | 40 | ) 41 | } 42 | if (meta.type === 'search') { 43 | return ( 44 | 45 | 46 | {decorateContent(meta.question, max)} 47 | 48 | ) 49 | } 50 | if (meta.type === 'memo') { 51 | return ( 52 | 53 | 58 | {decorateContent(meta.question, max)} 59 | 60 | ) 61 | } 62 | if (meta.type === 'image') { 63 | return ( 64 | 65 | { 67 | if (displayFull) { 68 | window.api.openPath(meta.src) 69 | } 70 | }} 71 | src={meta.value} 72 | class={'inline cursor-pointer rounded-sm ' + (displayFull ? 'max-h-14' : 'w-6')} 73 | /> 74 |
75 |
76 | ) 77 | } 78 | return null 79 | } 80 | -------------------------------------------------------------------------------- /src/renderer/src/pages/History/utils.ts: -------------------------------------------------------------------------------- 1 | export function decorateContent(c: string, max: number = 50) { 2 | if (c.length > max) { 3 | return c.slice(0, max) + ' ......' 4 | } 5 | return c 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Loading.tsx: -------------------------------------------------------------------------------- 1 | import Loading from '@renderer/components/ui/Loading' 2 | import { Show, createSignal } from 'solid-js' 3 | 4 | export default function () { 5 | // FEAT: 延迟300ms显示 6 | const [show, setShow] = createSignal(false) 7 | setTimeout(() => setShow(true), 300) 8 | return ( 9 | 10 |
11 | 12 |
13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Setting/Custom.tsx: -------------------------------------------------------------------------------- 1 | import Plus from '@renderer/assets/icon/base/Plus' 2 | import TrashIcon from '@renderer/assets/icon/TrashIcon' 3 | import EditInput from '@renderer/components/ui/EditInput' 4 | import Slider from '@renderer/components/ui/Slider' 5 | import { setCustomModels, settingStore } from '@renderer/store/setting' 6 | import { cloneDeep } from 'lodash' 7 | import { For, Show } from 'solid-js' 8 | 9 | export default function () { 10 | return ( 11 | <> 12 | 13 | {(c, index) => { 14 | let editBox: HTMLDivElement | undefined 15 | return ( 16 | <> 17 | 18 |
{ 20 | const arr = cloneDeep(settingStore.models.CustomModel.models) 21 | arr.splice(index(), 1) 22 | console.log(arr) 23 | setCustomModels(arr) 24 | }} 25 | onMouseEnter={() => { 26 | editBox!.style.opacity = '0.3' 27 | }} 28 | onMouseLeave={() => { 29 | editBox!.style.opacity = '' 30 | }} 31 | class="group flex cursor-pointer items-center gap-1" 32 | > 33 |
34 | 35 |
36 |
37 | 38 |
39 | { 43 | const arr = cloneDeep(settingStore.models.CustomModel.models) 44 | arr[index()].customModel = v.trim() 45 | setCustomModels(arr) 46 | }} 47 | /> 48 | { 52 | const arr = cloneDeep(settingStore.models.CustomModel.models) 53 | arr[index()].apiKey = v.trim() 54 | setCustomModels(arr) 55 | }} 56 | /> 57 | { 61 | const arr = cloneDeep(settingStore.models.CustomModel.models) 62 | arr[index()].baseURL = v.trim() 63 | setCustomModels(arr) 64 | }} 65 | /> 66 |
67 | 创造性/随机性 68 |
69 | { 73 | const arr = cloneDeep(settingStore.models.CustomModel.models) 74 | arr[index()].temperature = v 75 | setCustomModels(arr) 76 | }} 77 | /> 78 |
79 |
80 |
81 | 82 | ) 83 | }} 84 | 85 |
86 |
87 | { 92 | const arr = cloneDeep(settingStore.models.CustomModel.models) 93 | arr.push({ 94 | apiKey: '', 95 | baseURL: '', 96 | temperature: 0.3, 97 | customModel: '' 98 | }) 99 | setCustomModels(arr) 100 | }} 101 | /> 102 |
103 |
104 | 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Setting/theme.ts: -------------------------------------------------------------------------------- 1 | import { SettingFontFamily } from 'src/main/models/model' 2 | 3 | export const themeOptions = [ 4 | { 5 | label: 'Gomoon', 6 | value: 'gomoon-theme', 7 | slogan: '欢迎来到 Gomoon 月球基地' 8 | }, 9 | { 10 | label: '白月光', 11 | value: 'light-theme', 12 | slogan: '月光的白,宁静护眼' 13 | } 14 | ] 15 | 16 | export const fontFamilyOption: { 17 | label: string 18 | value: SettingFontFamily 19 | }[] = [ 20 | { 21 | label: '阿里方圆体', 22 | value: 'AlimamaFangyuan' 23 | }, 24 | { 25 | label: '小米Sans体', 26 | value: 'MiSans' 27 | }, 28 | { 29 | label: '默认字体', 30 | value: 'default' 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /src/renderer/src/pages/System.tsx: -------------------------------------------------------------------------------- 1 | import { useToast } from '@renderer/components/ui/Toast' 2 | import { getSystem } from '@renderer/lib/util' 3 | import { systemStore } from '@renderer/store/setting' 4 | import { createEffect } from 'solid-js' 5 | 6 | export default function System() { 7 | const toast = useToast() 8 | let haveToasted = false 9 | createEffect(() => { 10 | if (systemStore.updateStatus.haveDownloaded) { 11 | if (haveToasted || getSystem() === 'mac') return 12 | haveToasted = true 13 | toast.confirm('新版本下载完成,是否安装(升级后会自动启动)').then((res) => { 14 | if (res) { 15 | window.api.quitForUpdate() 16 | } 17 | }) 18 | } 19 | }) 20 | return <> 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/src/store/answer.ts: -------------------------------------------------------------------------------- 1 | import { ansAssistant } from '@renderer/lib/ai/langchain' 2 | import { createStore, produce } from 'solid-js/store' 3 | import { ulid } from 'ulid' 4 | import { ErrorDict } from '@renderer/lib/constant' 5 | import { extractMeta } from '@renderer/lib/ai/parseString' 6 | import { createEffect } from 'solid-js' 7 | 8 | import { consumedToken, setConsumedTokenForAns } from './input' 9 | 10 | let trash = { 11 | id: '', 12 | answer: '', 13 | question: '', 14 | consumedToken: 0 15 | } 16 | function initTrash() { 17 | trash = { 18 | id: '', 19 | answer: '', 20 | question: '', 21 | consumedToken: 0 22 | } 23 | } 24 | 25 | const [answerStore, setAnswerStore] = createStore<{ 26 | id: string 27 | answer: string 28 | question: string 29 | }>({ 30 | id: '', 31 | answer: '', 32 | question: '', 33 | ...JSON.parse(localStorage.getItem('answer_msgs') || '{}') 34 | }) 35 | 36 | createEffect(() => { 37 | localStorage.setItem('answer_msgs', JSON.stringify(answerStore)) 38 | }) 39 | 40 | const [ansStatus, setAnsStatus] = createStore({ 41 | isGenerating: false 42 | }) 43 | let controller: AbortController 44 | let ansID: string 45 | 46 | export function setGeneratingStatus(status: boolean) { 47 | setAnsStatus( 48 | produce((ansStatus) => { 49 | ansStatus.isGenerating = status 50 | }) 51 | ) 52 | } 53 | 54 | export async function genAns(q: string) { 55 | controller = new AbortController() 56 | const initialContent = '......' 57 | setAnswerStore('id', ulid()) 58 | setAnswerStore('answer', initialContent) 59 | setAnswerStore('question', q) 60 | setGeneratingStatus(true) 61 | const ID = ulid() 62 | ansID = ID 63 | let haveAnswer = false 64 | try { 65 | await ansAssistant({ 66 | question: extractMeta(q, true), 67 | newTokenCallback(content) { 68 | ID === ansID && 69 | setAnswerStore('answer', (ans) => { 70 | if (!haveAnswer) { 71 | haveAnswer = true 72 | return content 73 | } 74 | return ans + content 75 | }) 76 | }, 77 | endCallback(res) { 78 | let consumedToken = res.llmOutput?.estimatedTokenUsage?.totalTokens ?? 0 79 | !consumedToken && (consumedToken = res.llmOutput?.tokenUsage?.totalTokens) 80 | !consumedToken && (consumedToken = 0) 81 | ID === ansID && setGeneratingStatus(false) 82 | setConsumedTokenForAns(consumedToken) 83 | }, 84 | errorCallback(err) { 85 | if (ID !== ansID) return 86 | setAnswerStore('answer', (ans) => ans + ErrorDict(err as Error)) 87 | setGeneratingStatus(false) 88 | }, 89 | pauseSignal: controller.signal 90 | }) 91 | } catch (err) { 92 | if (!ansStatus.isGenerating) return 93 | setAnswerStore('answer', (ans) => { 94 | return ans === initialContent ? ErrorDict(err as Error) : ans + ErrorDict(err as Error) 95 | }) 96 | setGeneratingStatus(false) 97 | } 98 | } 99 | export async function stopGenAns() { 100 | controller?.abort() 101 | ansID = '' 102 | setGeneratingStatus(false) 103 | setConsumedTokenForAns(await window.api.getTokenNum(answerStore.question + answerStore.answer)) 104 | } 105 | export function reGenAns() { 106 | setAnswerStore('answer', '') 107 | setGeneratingStatus(true) 108 | genAns(answerStore.question) 109 | } 110 | 111 | export function clearAns() { 112 | stopGenAns() 113 | trash = { 114 | id: answerStore.id, 115 | answer: answerStore.answer, 116 | question: answerStore.question, 117 | consumedToken: consumedToken().ans 118 | } 119 | setConsumedTokenForAns(0) 120 | setAnswerStore('id', ulid()) 121 | setAnswerStore('answer', '') 122 | setAnswerStore('question', '') 123 | } 124 | 125 | export function restoreAns() { 126 | if (!trash.consumedToken) { 127 | return 128 | } 129 | setAnswerStore('answer', trash.answer) 130 | setAnswerStore('question', trash.question) 131 | setConsumedTokenForAns(trash.consumedToken) 132 | initTrash() 133 | } 134 | 135 | export { answerStore, setAnswerStore, ansStatus } 136 | -------------------------------------------------------------------------------- /src/renderer/src/store/assistants.ts: -------------------------------------------------------------------------------- 1 | import { createStore, produce } from 'solid-js/store' 2 | import { AssistantModel } from 'src/main/models/model' 3 | import { createMemo } from 'solid-js' 4 | import { cloneDeep } from 'lodash' 5 | import { getRandomString } from '@renderer/lib/util' 6 | 7 | import { changeMatchModel, userData } from './user' 8 | 9 | const [assistants, setAssistants] = createStore([]) 10 | 11 | const [assistantsStatus, setAssistantsStatus] = createStore<{ 12 | [key: string]: 'editing' | 'creating' | 'saved' 13 | }>({}) 14 | 15 | export async function loadAssistants() { 16 | setAssistants(await window.api.getAssistants()) 17 | setAssistantsStatus(assistants.reduce((a, b) => ({ ...a, [b.id]: 'saved' }), {})) 18 | setAssistantsStatus({ 19 | ...assistantsStatus, 20 | creating: 'creating' 21 | }) 22 | } 23 | 24 | export function createNewAssistant(type: 'chat' | 'ans') { 25 | if (assistants.findIndex((a) => a.id === 'creating') !== -1) return 26 | const newA: AssistantModel = { 27 | type, 28 | id: 'creating', 29 | avatar: getRandomString(5), 30 | name: '', 31 | version: 1, 32 | prompt: '' 33 | } 34 | setAssistants( 35 | produce((a) => { 36 | a.unshift(newA) 37 | }) 38 | ) 39 | } 40 | 41 | export function onEditAssistant(id: string) { 42 | if (id === 'creating') return 43 | setAssistantsStatus( 44 | produce((a) => { 45 | a[id] = 'editing' 46 | }) 47 | ) 48 | } 49 | 50 | export function onCancelEditAssistant(id: string) { 51 | if (id === 'creating') { 52 | setAssistants( 53 | produce((a) => { 54 | a.shift() 55 | }) 56 | ) 57 | } else { 58 | setAssistantsStatus( 59 | produce((a) => { 60 | a[id] = 'saved' 61 | }) 62 | ) 63 | } 64 | } 65 | 66 | export async function saveAssistant(a: AssistantModel) { 67 | if (a.id === 'creating') { 68 | await window.api.createAssistant(cloneDeep(a)) 69 | setAssistants( 70 | produce((as) => { 71 | as.shift() 72 | }) 73 | ) 74 | } else { 75 | await window.api.updateAssistant(cloneDeep(a)) 76 | } 77 | loadAssistants() 78 | } 79 | 80 | export async function deleteAssistant(id: string) { 81 | await window.api.deleteAssistant(id) 82 | loadAssistants() 83 | } 84 | 85 | export async function useAssistant(id: string) { 86 | await window.api.useAssistant(id) 87 | await loadAssistants() 88 | const currentA = assistants.find((a) => a.id === id) 89 | changeMatchModel(currentA?.matchModel, id) 90 | } 91 | 92 | export const getCurrentAssistantForAnswer = createMemo(() => { 93 | return ( 94 | assistants.find((a) => a.id === userData.selectedAssistantForAns) || { 95 | type: 'ans', 96 | id: 'default', 97 | name: '暂无助手', 98 | introduce: '', 99 | prompt: '', 100 | version: 0 101 | } 102 | ) 103 | }) 104 | 105 | export const getCurrentAssistantForChat = createMemo( 106 | () => 107 | assistants.find((a) => a.id === userData.selectedAssistantForChat) || { 108 | type: 'chat', 109 | id: 'default', 110 | name: '暂无助手', 111 | prompt: '', 112 | version: 0 113 | } 114 | ) 115 | 116 | export const exportAssistants = async () => { 117 | const json = JSON.stringify(assistants) 118 | await window.api.saveFile('assistants.json', json) 119 | } 120 | 121 | export const importAssistants = async (content: string) => { 122 | try { 123 | const importA = JSON.parse(content) 124 | if (!Array.isArray(importA)) return false 125 | // eslint-disable-next-line solid/reactivity 126 | importA.forEach(async (a: AssistantModel) => { 127 | if ( 128 | typeof a.id === 'string' && 129 | typeof a.name === 'string' && 130 | typeof a.prompt === 'string' && 131 | (a.type === 'ans' || a.type === 'chat') && 132 | typeof a.version === 'number' 133 | ) { 134 | const curA = assistants.find((as) => as.id === a.id) 135 | if (curA && curA.version >= a.version) return 136 | await saveAssistant(a) 137 | } else { 138 | throw new Error('invalid assistant') 139 | } 140 | }) 141 | return true 142 | } catch (e) { 143 | return false 144 | } 145 | } 146 | 147 | export { assistants, assistantsStatus } 148 | -------------------------------------------------------------------------------- /src/renderer/src/store/collection.ts: -------------------------------------------------------------------------------- 1 | import { MsgTypes } from '@renderer/components/Message' 2 | import { createStore } from 'solid-js/store' 3 | import { CollectionModel } from 'src/main/models/model' 4 | import { cloneDeep } from 'lodash' 5 | 6 | import { answerStore, setAnswerStore } from './answer' 7 | import { userData } from './user' 8 | import { msgs, setMsgMeta, setMsgs } from './chat' 9 | 10 | export const [collections, setCollections] = createStore([]) 11 | 12 | export async function loadCollection() { 13 | const res = await window.api.getCollections() 14 | setCollections(res) 15 | return collections 16 | } 17 | 18 | function getAnsContents(id: string): CollectionModel['contents'][number] { 19 | return [ 20 | { 21 | id, 22 | assistantId: userData.selectedAssistantForAns, 23 | type: 'ans', 24 | role: 'question', 25 | content: answerStore.question 26 | }, 27 | { 28 | id, 29 | assistantId: userData.selectedAssistantForAns, 30 | type: 'ans', 31 | role: 'ans', 32 | content: answerStore.answer 33 | } 34 | ] 35 | } 36 | 37 | function getChatContents(id: string): CollectionModel['contents'][number] { 38 | const index = msgs.findIndex((m) => m.id === id) 39 | return [ 40 | { 41 | id, 42 | assistantId: userData.selectedAssistantForChat, 43 | type: 'chat', 44 | role: 'human', 45 | content: msgs[index - 1].content 46 | }, 47 | { 48 | id, 49 | assistantId: userData.selectedAssistantForChat, 50 | type: 'chat', 51 | role: 'ai', 52 | content: msgs[index].content 53 | } 54 | ] 55 | } 56 | 57 | export async function createCollection(name: string, id: string, type: MsgTypes) { 58 | if (type === 'ans') { 59 | await window.api.createCollection({ 60 | name, 61 | contents: [getAnsContents(id)] 62 | }) 63 | } else { 64 | await window.api.createCollection({ 65 | name, 66 | contents: [getChatContents(id)] 67 | }) 68 | } 69 | loadCollection() 70 | } 71 | 72 | export async function addCollection(name: string, id: string, type: MsgTypes) { 73 | const c = cloneDeep(collections.find((c) => c.name === name)) 74 | if (!c) { 75 | return 76 | } 77 | if (type === 'ans') { 78 | c.contents.push(getAnsContents(id)) 79 | } else { 80 | c.contents.push(getChatContents(id)) 81 | } 82 | await window.api.updateCollection(c) 83 | loadCollection() 84 | } 85 | 86 | export async function updateCollection(id: string, index: number) { 87 | const c = cloneDeep(collections.find((c) => c.id === id)) 88 | if (c) { 89 | c.contents.splice(index, 1) 90 | await window.api.updateCollection(c) 91 | } 92 | loadCollection() 93 | } 94 | 95 | export async function removeCollection(id: string) { 96 | await window.api.deleteCollection(id) 97 | loadCollection() 98 | } 99 | 100 | export async function StickTop(id: string) { 101 | await window.api.stickTopCollection(id) 102 | loadCollection() 103 | } 104 | 105 | export async function witchToChat(c: CollectionModel['contents'][number]) { 106 | if (c[0].type === 'ans') { 107 | setAnswerStore('question', c[0].content) 108 | setAnswerStore('answer', c[1].content) 109 | } else { 110 | setMsgs( 111 | c.map((item) => ({ 112 | id: item.id!, 113 | role: item.role as 'human' | 'system' | 'ai', 114 | content: item.content 115 | })) 116 | ) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/renderer/src/store/history.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash' 2 | import { createStore } from 'solid-js/store' 3 | import { HistoryModel } from 'src/main/models/model' 4 | import { ulid } from 'ulid' 5 | 6 | import { answerStore, setAnswerStore } from './answer' 7 | import { Msg, msgMeta, msgs, setMsgMeta, setMsgs } from './chat' 8 | import { setSelectedAssistantForAns, setSelectedAssistantForChat } from './user' 9 | import { getCurrentAssistantForAnswer, getCurrentAssistantForChat } from './assistants' 10 | 11 | const [histories, setHistories] = createStore([]) 12 | 13 | export async function loadHistories() { 14 | return window.api.getHistories().then(setHistories) 15 | } 16 | 17 | export async function starHistory(historyID: string, status: boolean) { 18 | await window.api.setHistoryStar(historyID, status) 19 | loadHistories() 20 | return 21 | } 22 | 23 | export async function clearHistory() { 24 | await window.api.clearHistory() 25 | loadHistories() 26 | return 27 | } 28 | 29 | export async function addHistory(history: HistoryModel) { 30 | await window.api.addHistory(cloneDeep(history)) 31 | loadHistories() 32 | } 33 | 34 | export async function copyHistory(historyID: string) { 35 | const history = cloneDeep(histories.find((history) => history.id === historyID)!) 36 | const id = ulid() 37 | await window.api.addHistory({ 38 | ...history, 39 | id, 40 | starred: false 41 | }) 42 | loadHistories() 43 | } 44 | 45 | export async function removeHistory(historyID: string) { 46 | await window.api.deleteHistory(historyID) 47 | loadHistories() 48 | } 49 | 50 | export const historyManager: { 51 | formatHistory(type: 'chat' | 'ans'): HistoryModel 52 | drawHistory(history: HistoryModel) 53 | newHistory(type: 'chat' | 'ans'): Promise 54 | } = { 55 | formatHistory(type: 'chat' | 'ans') { 56 | if (type === 'ans') { 57 | return { 58 | id: answerStore.id, 59 | type: 'ans', 60 | assistantId: getCurrentAssistantForAnswer()?.id, 61 | contents: [ 62 | { 63 | role: 'question', 64 | content: answerStore.question 65 | }, 66 | { 67 | role: 'ans', 68 | content: answerStore.answer 69 | } 70 | ] 71 | } 72 | } else { 73 | return { 74 | id: msgMeta.id, 75 | type: 'chat', 76 | assistantId: getCurrentAssistantForChat()?.id, 77 | contents: cloneDeep(msgs) 78 | } 79 | } 80 | }, 81 | async drawHistory(h: HistoryModel) { 82 | if (h.type === 'ans') { 83 | if (answerStore.answer && answerStore.question) { 84 | await this.newHistory('ans') 85 | } 86 | setAnswerStore('id', h.id) 87 | setAnswerStore('question', h.contents[0].content) 88 | setAnswerStore('answer', h.contents[1].content) 89 | h.assistantId && setSelectedAssistantForAns(h.assistantId) 90 | } else if (h.type === 'chat') { 91 | msgs.length && (await this.newHistory('chat')) 92 | setMsgMeta('id', h.id) 93 | setMsgs(h.contents as Msg[]) 94 | h.assistantId && setSelectedAssistantForChat(h.assistantId) 95 | } 96 | }, 97 | async newHistory(type: 'chat' | 'ans') { 98 | const history = this.formatHistory(type) 99 | let starred = false 100 | const oldH = histories.find((h) => h.id === history.id) 101 | if (oldH) { 102 | starred = !!oldH.starred 103 | await removeHistory(history.id) 104 | } 105 | addHistory({ 106 | starred, 107 | ...history 108 | }) 109 | } 110 | } 111 | 112 | export { histories } 113 | -------------------------------------------------------------------------------- /src/renderer/src/store/input.ts: -------------------------------------------------------------------------------- 1 | import { createMemo } from 'solid-js' 2 | import { createStore } from 'solid-js/store' 3 | import { modelDict } from '@lib/langchain' 4 | 5 | import { userData } from './user' 6 | 7 | interface InputStore { 8 | isNetworking: boolean 9 | memoCapsule: boolean 10 | inputText?: string 11 | consumedToken: { 12 | ans: number 13 | chat: number 14 | } 15 | } 16 | 17 | const [inputStore, setInputStore] = createStore({ 18 | isNetworking: false, 19 | memoCapsule: false, 20 | inputText: '', 21 | consumedToken: { 22 | ans: 0, 23 | chat: 0 24 | } 25 | }) 26 | 27 | export function setNetworkingStatus(status: boolean) { 28 | setInputStore('isNetworking', status) 29 | } 30 | 31 | export const isNetworking = createMemo(() => { 32 | if (userData.selectedModel === 'ERNIE4') { 33 | return false 34 | } 35 | return inputStore.isNetworking 36 | }) 37 | 38 | export const memoCapsule = createMemo(() => { 39 | return inputStore.memoCapsule 40 | }) 41 | 42 | export function setMemoCapsule(status: boolean) { 43 | setInputStore('memoCapsule', status) 44 | } 45 | 46 | export function setInputText(text: string) { 47 | setInputStore('inputText', text) 48 | } 49 | 50 | export const inputText = createMemo(() => { 51 | return inputStore.inputText ?? '' 52 | }) 53 | 54 | export const tokens = createMemo(() => { 55 | function parseNum(num: number) { 56 | if (num < 1000) { 57 | return num 58 | } 59 | return `${Math.floor(num / 1000)}k` 60 | } 61 | return { 62 | maxToken: parseNum(modelDict[userData.selectedModel].maxToken), 63 | consumedTokenForChat: (plusNum: number) => parseNum(inputStore.consumedToken.chat + plusNum), 64 | consumedTokenForAns: (plusNum: number) => parseNum(inputStore.consumedToken.ans + plusNum) 65 | } 66 | }) 67 | export const consumedToken = createMemo(() => { 68 | return inputStore.consumedToken 69 | }) 70 | export function setConsumedTokenForChat(token: number) { 71 | setInputStore('consumedToken', 'chat', token) 72 | } 73 | export function setConsumedTokenForAns(token: number) { 74 | setInputStore('consumedToken', 'ans', token) 75 | } 76 | -------------------------------------------------------------------------------- /src/renderer/src/store/memo.ts: -------------------------------------------------------------------------------- 1 | import { createMemo } from 'solid-js' 2 | import { createStore, produce } from 'solid-js/store' 3 | import { MemoModel } from 'src/main/models/model' 4 | import { cloneDeep } from 'lodash' 5 | import { SaveMemoParams } from 'src/main/lib/ai/embedding' 6 | 7 | import { userData } from './user' 8 | 9 | const [memories, setMemories] = createStore([]) 10 | const [memoriesStatus, setMemoriesStatus] = createStore<{ 11 | [key: string]: 'editing' | 'creating' | 'saved' 12 | }>({}) 13 | export async function loadMemories() { 14 | setMemories(await window.api.getMemories()) 15 | setMemoriesStatus(memories.reduce((a, b) => ({ ...a, [b.id]: 'saved' }), {})) 16 | setMemoriesStatus({ 17 | ...memoriesStatus, 18 | creating: 'creating' 19 | }) 20 | } 21 | export async function initMemories() { 22 | await window.api.initMemories() 23 | await loadMemories() 24 | } 25 | export const getCurrentMemo = createMemo(() => { 26 | return ( 27 | memories.find((a) => a.id === userData.selectedMemo) || { 28 | id: 'default', 29 | name: '暂无记忆', 30 | version: 0, 31 | introduce: '', 32 | fragment: [] 33 | } 34 | ) 35 | }) 36 | 37 | export function createNewMemo() { 38 | if (memories.findIndex((m) => m.id === 'creating') !== -1) return 39 | const newM: MemoModel = { 40 | id: 'creating', 41 | name: '', 42 | version: 1, 43 | introduce: '', 44 | fragment: [] 45 | } 46 | setMemories( 47 | produce((a) => { 48 | a.unshift(newM) 49 | }) 50 | ) 51 | } 52 | export async function onEditMemo(id: string) { 53 | await window.api.editMemory(id, cloneDeep(memories.find((m) => m.id === id)?.fragment) || []) 54 | setMemoriesStatus( 55 | produce((m) => { 56 | m[id] = 'editing' 57 | }) 58 | ) 59 | } 60 | export function onCancelEditMemo(id: string) { 61 | window.api.cancelSaveMemory(id) 62 | if (id === 'creating') { 63 | setMemories( 64 | produce((m) => { 65 | m.shift() 66 | }) 67 | ) 68 | } else { 69 | setMemoriesStatus( 70 | produce((m) => { 71 | m[id] = 'saved' 72 | }) 73 | ) 74 | } 75 | } 76 | export async function saveMemo(m: SaveMemoParams) { 77 | await window.api.saveMemory(cloneDeep(m)) 78 | setMemories( 79 | produce((memo) => { 80 | memo.shift() 81 | }) 82 | ) 83 | loadMemories() 84 | } 85 | export async function useMemo(id: string) { 86 | await window.api.useMemory(id) 87 | await loadMemories() 88 | } 89 | export async function deleteMemo(id: string) { 90 | await window.api.deleteMemory(id) 91 | loadMemories() 92 | } 93 | export async function importMemo(path: string) { 94 | if (!(await window.api.importMemory(path))) { 95 | return false 96 | } 97 | loadMemories() 98 | return true 99 | } 100 | export { memories, memoriesStatus } 101 | -------------------------------------------------------------------------------- /src/renderer/src/store/user.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'solid-js/store' 2 | import { AssistantModel, Line, UserDataModel } from 'src/main/models/model' 3 | import { ModelsType } from '@lib/langchain' 4 | import { createMemo } from 'solid-js' 5 | 6 | import { assistants, useAssistant } from './assistants' 7 | import { memories, useMemo } from './memo' 8 | import { setCustomModelSelected } from './setting' 9 | 10 | /** 11 | * @abstract 所有不在设置页面的数据 12 | */ 13 | 14 | /** 15 | * FEAT: Lines 16 | */ 17 | const [lines, setLines] = createStore([]) 18 | 19 | export async function loadLines() { 20 | const l = await window.api.getLines() 21 | setLines( 22 | l.map((line) => ({ from: 'Gomoon', content: '快速双击复制,可以直接进入问答模式', ...line })) 23 | ) 24 | } 25 | export const currentLines = createMemo(() => { 26 | return lines 27 | }) 28 | 29 | const [userData, setUserData] = createStore({ 30 | firstTime: true, 31 | selectedModel: 'GPT4', 32 | selectedAssistantForAns: '', 33 | selectedAssistantForChat: '', 34 | selectedMemo: '', 35 | firstTimeFor: { 36 | modelSelect: true, 37 | assistantSelect: true 38 | }, 39 | windowSize: { 40 | width: 0, 41 | height: 0 42 | } 43 | }) 44 | 45 | export const [userState, setUserState] = createStore({ 46 | preSelectedAssistant: '' 47 | }) 48 | 49 | export async function loadUserData() { 50 | setUserData(await window.api.getUserData()) 51 | loadLines() 52 | } 53 | 54 | export function userHasUse() { 55 | window.api.setUserData({ 56 | firstTime: false 57 | }) 58 | } 59 | 60 | export function changeMatchModel(model: AssistantModel['matchModel'], id: string) { 61 | if (model && model !== 'current') { 62 | if (model.startsWith('CustomModel')) { 63 | const target = model.slice(12) 64 | model = 'CustomModel' 65 | setCustomModelSelected(target) 66 | } 67 | window.api 68 | .setUserData({ 69 | selectedModel: model 70 | }) 71 | .then(() => { 72 | setUserData('selectedModel', model as ModelsType) 73 | }) 74 | } 75 | setUserState('preSelectedAssistant', id) 76 | } 77 | 78 | export function setSelectedModel(model: ModelsType) { 79 | window.api 80 | .setUserData({ 81 | selectedModel: model 82 | }) 83 | .then(() => { 84 | setUserData('selectedModel', model) 85 | }) 86 | } 87 | 88 | export async function setSelectedAssistantForAns(assistantID: string) { 89 | if (assistants.findIndex((item) => item.id === assistantID) === -1) { 90 | return 91 | } 92 | setUserData('selectedAssistantForAns', assistantID) 93 | await useAssistant(assistantID) 94 | return window.api.setUserData({ 95 | selectedAssistantForAns: assistantID 96 | }) 97 | } 98 | 99 | export async function setSelectedAssistantForChat(assistantID: string) { 100 | if (assistants.findIndex((item) => item.id === assistantID) === -1) { 101 | return 102 | } 103 | setUserData('selectedAssistantForChat', assistantID) 104 | await useAssistant(assistantID) 105 | return window.api.setUserData({ 106 | selectedAssistantForChat: assistantID 107 | }) 108 | } 109 | 110 | export async function setSelectedMemo(memoID: string) { 111 | if (memories.findIndex((item) => item.id === memoID) === -1) { 112 | return 113 | } 114 | setUserData('selectedMemo', memoID) 115 | await useMemo(memoID) 116 | return window.api.setUserData({ 117 | selectedMemo: memoID 118 | }) 119 | } 120 | 121 | export async function hasFirstTimeFor(key: keyof UserDataModel['firstTimeFor']) { 122 | setUserData('firstTimeFor', { 123 | ...userData.firstTimeFor, 124 | [key]: false 125 | }) 126 | await window.api.setUserData({ 127 | firstTimeFor: { 128 | [key]: false 129 | } 130 | }) 131 | loadUserData() 132 | } 133 | 134 | const [pageData, setPageData] = createStore({ 135 | isSpeech: false 136 | }) 137 | 138 | export { userData, pageData, setPageData } 139 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "include": [], 4 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/*", "src/lib/**/*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["electron-vite/node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/renderer/src/env.d.ts", 5 | "src/renderer/src/**/*", 6 | "src/renderer/src/**/*.json", 7 | "src/renderer/src/**/*.tsx", 8 | "src/lib/**/*", 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "jsxImportSource": "solid-js", 13 | "baseUrl": ".", 14 | "paths": { 15 | "@renderer/*": [ 16 | "src/renderer/src/*" 17 | ], 18 | "@lib/*": [ 19 | "src/lib/*" 20 | ] 21 | } 22 | } 23 | } 24 | --------------------------------------------------------------------------------