├── .erb ├── configs │ ├── .eslintrc │ ├── webpack.config.base.ts │ ├── webpack.config.eslint.ts │ ├── webpack.config.main.prod.ts │ ├── webpack.config.preload.dev.ts │ ├── webpack.config.renderer.dev.dll.ts │ ├── webpack.config.renderer.dev.ts │ ├── webpack.config.renderer.prod.ts │ └── webpack.paths.ts ├── img │ ├── erb-banner.svg │ ├── erb-logo.png │ └── palette-sponsor-banner.svg ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── check-build-exists.ts │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── delete-source-maps.js │ ├── electron-rebuild.js │ ├── link-modules.ts │ └── notarize.js ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── config.yml ├── dependabot.yml └── stale.yml ├── .gitignore ├── .node-version ├── .prettierrc ├── LICENSE ├── README.md ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── icon-1024.png ├── icon-raw.png ├── icon.icns ├── icon.ico ├── icon.png ├── icon.svg ├── iconTemplate.png ├── iconTemplate@2x.png ├── iconTemplateRaw.png ├── iconTemplateRawPreview.png ├── icon_pro.png ├── icon_pro2.png ├── icon_pro_plus.png └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png ├── doc ├── FAQ-CN.md ├── FAQ.md ├── README-CN.md └── statics │ ├── android.png │ ├── app_store.webp │ ├── demo.png │ ├── demo2.png │ ├── demo3.gif │ ├── demo_desktop_1.jpg │ ├── demo_desktop_2.jpg │ ├── demo_desktop_3.jpg │ ├── google_play.png │ ├── google_play.webp │ ├── icon.png │ ├── linux.png │ ├── mac.png │ ├── snapshot2.png │ ├── snapshot4.png │ ├── snapshot_dark.png │ ├── snapshot_light.png │ └── windows.png ├── electron-builder.yml ├── icons ├── icon-128.webp ├── icon-192.webp ├── icon-256.webp ├── icon-48.webp ├── icon-512.webp ├── icon-72.webp └── icon-96.webp ├── jest.config.ts ├── package-lock.json ├── package.json ├── postcss.config.js ├── release └── app │ ├── package-lock.json │ └── package.json ├── resources ├── icon-background.png ├── icon-foreground.png ├── icon-only.png ├── splash-dark.png └── splash.png ├── src ├── __tests__ │ └── App.test.tsx.bk ├── main │ ├── analystic-node.ts │ ├── autoLauncher.ts │ ├── file-parser.ts │ ├── locales.ts │ ├── main.ts │ ├── menu.ts │ ├── preload.ts │ ├── proxy.ts │ ├── readability.ts │ ├── store-node.ts │ ├── util.ts │ └── window_state.ts ├── renderer │ ├── Sidebar.tsx │ ├── components │ │ ├── Accordion.tsx │ │ ├── Artifact.tsx │ │ ├── Attachments.tsx │ │ ├── ConfirmDeleteButton.tsx │ │ ├── CreatableSelect.tsx │ │ ├── CustomProviderIcon.tsx │ │ ├── EditableAvatar.tsx │ │ ├── ExitFullscreenButton.tsx │ │ ├── FileIcon.tsx │ │ ├── Header.tsx │ │ ├── Image.tsx │ │ ├── ImageCountSlider.tsx │ │ ├── ImageModelSelect.tsx │ │ ├── ImageStyleSelect.tsx │ │ ├── InputBox.tsx │ │ ├── LazySlider.tsx │ │ ├── Link.tsx │ │ ├── Mark.tsx │ │ ├── Markdown.tsx │ │ ├── MaxContextMessageCountSlider.tsx │ │ ├── Mermaid.tsx │ │ ├── Message.tsx │ │ ├── MessageErrTips.tsx │ │ ├── MessageList.tsx │ │ ├── MessageLoading.tsx │ │ ├── MiniButton.tsx │ │ ├── ModelSelectorNew.tsx │ │ ├── Page.tsx │ │ ├── PasswordTextField.tsx │ │ ├── PopoverConfirm.tsx │ │ ├── SessionItem.tsx │ │ ├── SessionList.tsx │ │ ├── Shortcut.tsx │ │ ├── SimpleSelect.tsx │ │ ├── SliderWithInput.tsx │ │ ├── SortableItem.tsx │ │ ├── SponsorChip.tsx │ │ ├── StyledMenu.tsx │ │ ├── TemperatureSlider.tsx │ │ ├── TextFieldReset.tsx │ │ ├── ThreadHistoryDrawer.tsx │ │ ├── Toasts.tsx │ │ ├── Toolbar.tsx │ │ ├── TopPSlider.tsx │ │ ├── UpdateAvailableButton.tsx │ │ ├── icons │ │ │ ├── ArrowRightIcon.tsx │ │ │ ├── BrandGithub.tsx │ │ │ ├── BrandRedNote.tsx │ │ │ ├── BrandWechat.tsx │ │ │ ├── BrandX.tsx │ │ │ ├── FullscreenIcon.tsx │ │ │ ├── Loading.tsx │ │ │ └── ProviderIcon.tsx │ │ ├── message-parts │ │ │ └── ToolCallPartUI.tsx │ │ └── ui │ │ │ ├── command.tsx │ │ │ └── dialog.tsx │ ├── favicon.ico │ ├── hooks │ │ ├── dom.ts │ │ ├── useAppTheme.ts │ │ ├── useChatboxAIModels.ts │ │ ├── useCopilots.ts │ │ ├── useDefaultSystemLanguage.ts │ │ ├── useI18nEffect.ts │ │ ├── useNeedRoomForWinControls.ts │ │ ├── useProviders.ts │ │ ├── useScreenChange.ts │ │ ├── useSettings.ts │ │ ├── useShortcut.tsx │ │ └── useVersion.ts │ ├── i18n │ │ ├── changelogs │ │ │ ├── changelog_en.ts │ │ │ ├── changelog_zh_Hans.ts │ │ │ └── changelog_zh_Hant.ts │ │ ├── index.ts │ │ ├── locales.ts │ │ ├── locales │ │ │ ├── ar │ │ │ │ └── translation.json │ │ │ ├── de │ │ │ │ └── translation.json │ │ │ ├── en │ │ │ │ └── translation.json │ │ │ ├── es │ │ │ │ └── translation.json │ │ │ ├── fr │ │ │ │ └── translation.json │ │ │ ├── it-IT │ │ │ │ └── translation.json │ │ │ ├── ja │ │ │ │ └── translation.json │ │ │ ├── ko │ │ │ │ └── translation.json │ │ │ ├── nb-NO │ │ │ │ └── translation.json │ │ │ ├── pt-PT │ │ │ │ └── translation.json │ │ │ ├── ru │ │ │ │ └── translation.json │ │ │ ├── sv │ │ │ │ └── translation.json │ │ │ ├── zh-Hans │ │ │ │ └── translation.json │ │ │ └── zh-Hant │ │ │ │ └── translation.json │ │ └── parser.ts │ ├── index.ejs │ ├── index.tsx │ ├── index.web.ejs │ ├── lib │ │ ├── format-chat.tsx │ │ └── utils.ts │ ├── modals │ │ ├── AppStoreRating.tsx │ │ ├── ArtifactPreview.tsx │ │ ├── AttachLink.tsx │ │ ├── ClearSessionList.tsx │ │ ├── ExportChat.tsx │ │ ├── MessageEdit.tsx │ │ ├── ModelEdit.tsx │ │ ├── ProviderSelector.tsx │ │ ├── ReportContent.tsx │ │ ├── SessionSettings.tsx │ │ ├── Welcome.tsx │ │ └── index.tsx │ ├── packages │ │ ├── apple_app_store.ts │ │ ├── base64.test.ts │ │ ├── base64.ts │ │ ├── cache.ts │ │ ├── codeblock_state_recorder.ts │ │ ├── event.ts │ │ ├── filetype.ts │ │ ├── initial_data.ts │ │ ├── keypairs.ts │ │ ├── latex.ts │ │ ├── lemonsqueezy.ts │ │ ├── local-parser.ts │ │ ├── model-calls │ │ │ ├── index.ts │ │ │ ├── stream-text.ts │ │ │ └── tools.ts │ │ ├── model-setting-utils │ │ │ ├── azure-setting-util.ts │ │ │ ├── base-config.ts │ │ │ ├── chatboxai-setting-util.ts │ │ │ ├── chatglm-setting-util.ts │ │ │ ├── claude-setting-util.ts │ │ │ ├── custom-setting-util.ts │ │ │ ├── deepseek-setting-util.ts │ │ │ ├── gemini-setting-util.ts │ │ │ ├── groq-setting-util.ts │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ ├── lmstudio-setting-util.ts │ │ │ ├── ollama-setting-util.ts │ │ │ ├── openai-setting-util.ts │ │ │ ├── perplexity-setting-util.ts │ │ │ ├── siliconflow-setting-util.ts │ │ │ └── xai-setting-util.ts │ │ ├── models │ │ │ ├── abstract-ai-sdk.ts │ │ │ ├── azure.ts │ │ │ ├── chatboxai.ts │ │ │ ├── chatglm.ts │ │ │ ├── claude.ts │ │ │ ├── custom-openai.ts │ │ │ ├── deepseek.ts │ │ │ ├── errors.ts │ │ │ ├── gemini.ts │ │ │ ├── groq.ts │ │ │ ├── index.ts │ │ │ ├── llm_utils.test.ts │ │ │ ├── llm_utils.ts │ │ │ ├── lmstudio.ts │ │ │ ├── ollama.ts │ │ │ ├── openai-compatible.ts │ │ │ ├── openai.ts │ │ │ ├── perplexity.ts │ │ │ ├── siliconflow.ts │ │ │ ├── types.ts │ │ │ └── xai.ts │ │ ├── navigator.ts │ │ ├── pic_utils.ts │ │ ├── prompts.ts │ │ ├── remote.ts │ │ ├── request.ts │ │ ├── token.tsx │ │ ├── token_config.ts │ │ ├── web-search │ │ │ ├── base.ts │ │ │ ├── bing-news.ts │ │ │ ├── bing.ts │ │ │ ├── chatbox-search.ts │ │ │ ├── duckduckgo.ts │ │ │ ├── index.ts │ │ │ └── tavily.ts │ │ └── word-count.ts │ ├── pages │ │ ├── CleanWindow.tsx │ │ ├── PictureDialog.tsx │ │ ├── RemoteDialogWindow.tsx │ │ ├── SearchDialog.tsx │ │ └── SettingDialog │ │ │ └── AdvancedSettingTab.tsx │ ├── platform │ │ ├── desktop_platform.ts │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── web_exporter.ts │ │ ├── web_platform.ts │ │ └── web_platform_utils.ts │ ├── preload.d.ts │ ├── reportWebVitals.ts │ ├── router.tsx │ ├── routes │ │ ├── __root.tsx │ │ ├── about.tsx │ │ ├── copilots.tsx │ │ ├── index.tsx │ │ ├── session │ │ │ └── $sessionId.tsx │ │ └── settings │ │ │ ├── chat.tsx │ │ │ ├── default-models.tsx │ │ │ ├── general.tsx │ │ │ ├── hotkeys.tsx │ │ │ ├── index.tsx │ │ │ ├── provider │ │ │ ├── $providerId.tsx │ │ │ ├── chatbox-ai.tsx │ │ │ ├── index.tsx │ │ │ └── route.tsx │ │ │ ├── route.tsx │ │ │ └── web-search.tsx │ ├── setup │ │ ├── ga_init.ts │ │ ├── init_data.ts │ │ ├── load_polyfill.ts │ │ ├── mobile_safe_area.ts │ │ ├── protect.ts │ │ ├── sentry_init.ts │ │ └── storage_clear.ts │ ├── setupTests.ts │ ├── static │ │ ├── Block.css │ │ ├── _headers │ │ ├── fonts │ │ │ ├── Cairo-Bold.ttf │ │ │ └── Cairo-Regular.ttf │ │ ├── globals.css │ │ ├── icon.png │ │ ├── icons │ │ │ ├── icons8-c-48.png │ │ │ ├── icons8-c-sharp-48.png │ │ │ ├── icons8-cpp-48.png │ │ │ ├── icons8-css-48.png │ │ │ ├── icons8-csv-48.png │ │ │ ├── icons8-golang-48.png │ │ │ ├── icons8-html-48.png │ │ │ ├── icons8-java-48.png │ │ │ ├── icons8-javascript-48.png │ │ │ ├── icons8-json-48.png │ │ │ ├── icons8-markdown-48.png │ │ │ ├── icons8-pdf-48.png │ │ │ ├── icons8-php-48.png │ │ │ ├── icons8-ppt-48.png │ │ │ ├── icons8-python-48.png │ │ │ ├── icons8-ruby-48.png │ │ │ ├── icons8-rust-48.png │ │ │ ├── icons8-shell-48.png │ │ │ ├── icons8-swift-48.png │ │ │ ├── icons8-typescript-48.png │ │ │ ├── icons8-word-file-48.png │ │ │ ├── icons8-xls-48.png │ │ │ ├── icons8-xml-48.png │ │ │ └── providers │ │ │ │ ├── azure.png │ │ │ │ ├── chatbox-ai.png │ │ │ │ ├── chatglm-6b.png │ │ │ │ ├── claude.png │ │ │ │ ├── deepseek.png │ │ │ │ ├── gemini.png │ │ │ │ ├── groq.png │ │ │ │ ├── lm-studio.png │ │ │ │ ├── ollama.png │ │ │ │ ├── openai.png │ │ │ │ ├── perplexity.png │ │ │ │ ├── siliconflow.png │ │ │ │ └── xAI.png │ │ ├── index.css │ │ └── wechat_qrcode.png │ ├── storage │ │ ├── BaseStorage.ts │ │ ├── StoreStorage.ts │ │ └── index.ts │ ├── stores │ │ ├── atoms │ │ │ ├── configAtoms.ts │ │ │ ├── copilotAtoms.ts │ │ │ ├── index.ts │ │ │ ├── sessionAtoms.ts │ │ │ ├── settingsAtoms.ts │ │ │ ├── throttleWriteSessionAtom.ts │ │ │ ├── uiAtoms.ts │ │ │ └── utilAtoms.ts │ │ ├── migration.ts │ │ ├── premiumActions.ts │ │ ├── queryClient.ts │ │ ├── scrollActions.ts │ │ ├── sessionActions.ts │ │ ├── sessionStorageMutations.ts │ │ ├── settingActions.ts │ │ └── toastActions.ts │ ├── utils │ │ ├── image.ts │ │ ├── message.test.ts │ │ ├── message.ts │ │ ├── request.ts │ │ └── session-utils.ts │ └── variables.ts └── shared │ ├── constants.ts │ ├── defaults.ts │ ├── electron-types.ts │ ├── file-extensions.ts │ └── types.ts ├── tailwind.config.js ├── team-sharing ├── Caddyfile ├── Dockerfile ├── README-CN.md ├── README.md ├── demo_http.png ├── demo_https.png └── main.sh └── tsconfig.json /.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import webpack from 'webpack' 6 | import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin' 7 | import webpackPaths from './webpack.paths' 8 | import { dependencies as externals } from '../../release/app/package.json' 9 | 10 | const configuration: webpack.Configuration = { 11 | externals: [...Object.keys(externals || {})], 12 | 13 | stats: 'errors-only', 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.[jt]sx?$/, 19 | exclude: [/node_modules/, /\.d\.ts$/], 20 | use: { 21 | loader: 'ts-loader', 22 | options: { 23 | // Remove this line to enable type checking in webpack builds 24 | transpileOnly: true, 25 | compilerOptions: { 26 | module: 'esnext', 27 | }, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | 34 | output: { 35 | path: webpackPaths.srcPath, 36 | // https://github.com/webpack/webpack/issues/1114 37 | library: { 38 | type: 'commonjs2', 39 | }, 40 | }, 41 | 42 | /** 43 | * Determine the array of extensions that should be used to resolve modules. 44 | */ 45 | resolve: { 46 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 47 | modules: [webpackPaths.srcPath, 'node_modules'], 48 | // There is no need to add aliases here, the paths in tsconfig get mirrored 49 | plugins: [new TsconfigPathsPlugins()], 50 | }, 51 | 52 | plugins: [ 53 | new webpack.EnvironmentPlugin({ 54 | NODE_ENV: 'production', 55 | CHATBOX_BUILD_TARGET: 'unknown', 56 | CHATBOX_BUILD_PLATFORM: 'unknown', 57 | USE_LOCAL_API: '', 58 | }), 59 | ], 60 | } 61 | 62 | export default configuration 63 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default 4 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack' 6 | import path from 'path' 7 | import { merge } from 'webpack-merge' 8 | import baseConfig from './webpack.config.base' 9 | import webpackPaths from './webpack.paths' 10 | import { dependencies } from '../../package.json' 11 | import checkNodeEnv from '../scripts/check-node-env' 12 | 13 | checkNodeEnv('development') 14 | 15 | const dist = webpackPaths.dllPath 16 | 17 | const configuration: webpack.Configuration = { 18 | context: webpackPaths.rootPath, 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}), 35 | }, 36 | 37 | output: { 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | library: { 41 | name: 'renderer', 42 | type: 'var', 43 | }, 44 | }, 45 | 46 | plugins: [ 47 | new webpack.DllPlugin({ 48 | path: path.join(dist, '[name].json'), 49 | name: '[name]', 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'development', 63 | }), 64 | 65 | new webpack.LoaderOptionsPlugin({ 66 | debug: true, 67 | options: { 68 | context: webpackPaths.srcPath, 69 | output: { 70 | path: webpackPaths.dllPath, 71 | }, 72 | }, 73 | }), 74 | ], 75 | } 76 | 77 | export default merge(baseConfig, configuration) 78 | -------------------------------------------------------------------------------- /.erb/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const rootPath = path.join(__dirname, '../..') 4 | 5 | const dllPath = path.join(__dirname, '../dll') 6 | 7 | const srcPath = path.join(rootPath, 'src') 8 | const srcMainPath = path.join(srcPath, 'main') 9 | const srcRendererPath = path.join(srcPath, 'renderer') 10 | 11 | const releasePath = path.join(rootPath, 'release') 12 | const appPath = path.join(releasePath, 'app') 13 | const appPackagePath = path.join(appPath, 'package.json') 14 | const appNodeModulesPath = path.join(appPath, 'node_modules') 15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules') 16 | 17 | const distPath = path.join(appPath, 'dist') 18 | const distMainPath = path.join(distPath, 'main') 19 | const distRendererPath = path.join(distPath, 'renderer') 20 | 21 | const buildPath = path.join(releasePath, 'build') 22 | 23 | export default { 24 | rootPath, 25 | dllPath, 26 | srcPath, 27 | srcMainPath, 28 | srcRendererPath, 29 | releasePath, 30 | appPath, 31 | appPackagePath, 32 | appNodeModulesPath, 33 | srcNodeModulesPath, 34 | distPath, 35 | distMainPath, 36 | distRendererPath, 37 | buildPath, 38 | } 39 | -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub' 2 | -------------------------------------------------------------------------------- /.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path' 3 | import chalk from 'chalk' 4 | import fs from 'fs' 5 | import webpackPaths from '../configs/webpack.paths' 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js') 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js') 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold('The main process is not built yet. Build it by running "npm run build:main"') 13 | ) 14 | } 15 | 16 | if (!fs.existsSync(rendererPath)) { 17 | throw new Error( 18 | chalk.whiteBright.bgRed.bold( 19 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"' 20 | ) 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import chalk from 'chalk' 3 | import { execSync } from 'child_process' 4 | import { dependencies } from '../../package.json' 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies) 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)) 11 | if (nativeDeps.length === 0) { 12 | process.exit(0) 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 20 | ) 21 | const rootDependencies = Object.keys(dependenciesObject) 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency) 24 | ) 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold('Webpack does not work with native dependencies.')} 29 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 30 | plural ? 'are native dependencies' : 'is a native dependency' 31 | } and should be installed inside of the "./release/app" folder. 32 | First, uninstall the packages from "./package.json": 33 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 34 | ${chalk.bold('Then, instead of installing the package to the root "./package.json":')} 35 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 36 | ${chalk.bold('Install the package to "./release/app/package.json"')} 37 | ${chalk.whiteBright.bgGreen.bold('cd ./release/app && npm install your-package')} 38 | Read more about native dependencies at: 39 | ${chalk.bold('https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure')} 40 | `) 41 | process.exit(1) 42 | } 43 | } catch (e) { 44 | console.log('Native dependencies could not be checked') 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set') 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold(`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`) 11 | ) 12 | process.exit(2) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import detectPort from 'detect-port' 3 | 4 | const port = process.env.PORT || '1212' 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start` 11 | ) 12 | ) 13 | } else { 14 | process.exit(0) 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import { rimrafSync } from 'rimraf' 2 | import fs from 'fs' 3 | import webpackPaths from '../configs/webpack.paths' 4 | 5 | const foldersToRemove = [webpackPaths.distPath, webpackPaths.buildPath, webpackPaths.dllPath] 6 | 7 | foldersToRemove.forEach((folder) => { 8 | if (fs.existsSync(folder)) rimrafSync(folder) 9 | }) 10 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { rimrafSync } from 'rimraf' 4 | import webpackPaths from '../configs/webpack.paths' 5 | 6 | export default function deleteSourceMaps() { 7 | if (fs.existsSync(webpackPaths.distMainPath)) 8 | rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), { 9 | glob: true, 10 | }) 11 | if (fs.existsSync(webpackPaths.distRendererPath)) 12 | rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), { 13 | glob: true, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | import fs from 'fs' 3 | import { dependencies } from '../../release/app/package.json' 4 | import webpackPaths from '../configs/webpack.paths' 5 | 6 | if (Object.keys(dependencies || {}).length > 0 && fs.existsSync(webpackPaths.appNodeModulesPath)) { 7 | const electronRebuildCmd = 8 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .' 9 | const cmd = process.platform === 'win32' ? electronRebuildCmd.replace(/\//g, '\\') : electronRebuildCmd 10 | execSync(cmd, { 11 | cwd: webpackPaths.appPath, 12 | stdio: 'inherit', 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import webpackPaths from '../configs/webpack.paths' 3 | 4 | const { srcNodeModulesPath } = webpackPaths 5 | const { appNodeModulesPath } = webpackPaths 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction') 9 | } 10 | -------------------------------------------------------------------------------- /.erb/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize') 2 | 3 | exports.default = async function notarizeMacos(context) { 4 | const { electronPlatformName, appOutDir } = context 5 | if (electronPlatformName !== 'darwin') { 6 | return 7 | } 8 | 9 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env && 'APPLE_TEAM_ID' in process.env)) { 10 | console.warn('Skipping notarizing step. APPLE_ID, APPLE_ID_PASS and APPLE_TEAM_ID env variables must be set') 11 | return 12 | } 13 | 14 | const appName = context.packager.appInfo.productFilename 15 | 16 | console.log('[Notarize] start macOS notarization: notarize.js running with notarytool') 17 | 18 | await notarize({ 19 | tool: 'notarytool', 20 | appBundleId: 'xyz.chatboxapp.app', 21 | appPath: `${appOutDir}/${appName}.app`, 22 | appleId: process.env.APPLE_ID, 23 | appleIdPassword: process.env.APPLE_ID_PASS, 24 | teamId: process.env.APPLE_TEAM_ID, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # 暂时关掉 eslint 2 | * 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | .eslintcache 16 | 17 | # Dependency directory 18 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 19 | node_modules 20 | 21 | # OSX 22 | .DS_Store 23 | 24 | release/app/dist 25 | release/build 26 | .erb/dll 27 | 28 | .idea 29 | npm-debug.log.* 30 | *.css.d.ts 31 | *.sass.d.ts 32 | *.scss.d.ts 33 | 34 | # eslint ignores hidden directories by default: 35 | # https://github.com/eslint/eslint/issues/8429 36 | !.erb 37 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | plugins: ['@typescript-eslint'], 4 | rules: { 5 | // A temporary hack related to IDE not resolving correct package.json 6 | 'import/no-extraneous-dependencies': 'off', 7 | 'react/react-in-jsx-scope': 'off', 8 | 'react/jsx-filename-extension': 'off', 9 | 'import/extensions': 'off', 10 | 'import/no-unresolved': 'off', 11 | 'import/no-import-module-exports': 'off', 12 | 'no-shadow': 'off', 13 | '@typescript-eslint/no-shadow': 'error', 14 | 'no-unused-vars': 'off', 15 | '@typescript-eslint/no-unused-vars': 'error', 16 | }, 17 | parserOptions: { 18 | ecmaVersion: 2020, 19 | sourceType: 'module', 20 | project: './tsconfig.json', 21 | tsconfigRootDir: __dirname, 22 | createDefaultProgram: true, 23 | }, 24 | settings: { 25 | 'import/resolver': { 26 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 27 | node: {}, 28 | webpack: { 29 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 30 | }, 31 | typescript: {}, 32 | }, 33 | 'import/parsers': { 34 | '@typescript-eslint/parser': ['.ts', '.tsx'], 35 | }, 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Bin-Huang 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve / BUG 反馈(提交前请搜索是否存在重复issues) 4 | title: '[BUG]' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Bug Description** 10 | Please provide a clear and concise description of what the bug is. 11 | 12 | **Steps to Reproduce** 13 | Please provide the steps to reproduce the bug: 14 | 15 | 1. Go to "..." 16 | 2. Click on "..." 17 | 3. Scroll down to "..." 18 | 4. Observe the bug. 19 | 20 | **Expected Results** 21 | Please provide a clear and concise description of what you expected to happen. 22 | 23 | **Actual Results** 24 | Please provide a clear and concise description of what actually happened. 25 | 26 | **Screenshots** 27 | If possible, please add screenshots to help explain the issue. 28 | 29 | **Desktop (please complete the following information):** 30 | 31 | - Operating System: [e.g. macOS] 32 | - Application Version: [e.g. 2.0.1] 33 | 34 | **Additional Context** 35 | Please provide any additional context about the issue, such as interactions with other software or applications. 36 | 37 | --- 38 | 39 | **Bug 描述** 40 | 清晰简洁地描述这个 bug 是什么。 41 | 42 | **重现步骤** 43 | 请提供能够让我们重现这个 bug 的步骤: 44 | 45 | 1. 前往 "......" 46 | 2. 点击 "......" 47 | 3. 滚动到 "......" 48 | 4. 发现了这个 bug。 49 | 50 | **期望结果** 51 | 请清晰简洁地描述预期的行为。 52 | 53 | **实际结果** 54 | 请清晰简洁地描述实际的行为。 55 | 56 | **截图** 57 | 如果可行,添加截图以帮助解释问题。 58 | 59 | **桌面端(请填写以下信息):** 60 | 61 | - 操作系统:[例如 macOS] 62 | - 应用程序版本:[例如 2.0.1] 63 | 64 | **其他上下文** 65 | 在这里提供关于问题的任何其他上下文,例如与其他软件或应用程序的交互等。 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. / 其他建设性意见与讨论 4 | title: '[Other]' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project / 新功能新特性的想法(提交前请检查是否有重复 issues) 4 | title: '[Feature]' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Problem Description** 10 | Please describe the issue or difficulty you are experiencing and why it makes using the software difficult or frustrating. 11 | 12 | **Proposed Solution** 13 | Please provide a clear and concise description of what you would like to see in terms of a function or solution. 14 | 15 | **Additional Context** 16 | Please provide any additional context or information that would help better understanding your feature request, such as screenshots, examples, or use cases. 17 | 18 | --- 19 | 20 | **问题描述** 21 | 请描述您遇到的问题或难题,以及为什么这使得使用软件变得困难或令人沮丧。 22 | 23 | **解决思路** 24 | 请提供一个清晰、简洁的描述,说明您希望看到的功能或解决方案。 25 | 26 | **附加上下文** 27 | 请提供任何其他上下文或信息,以便更好地理解您的功能请求,例如截图、示例或用例。 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | [Please provide a detailed description of your contribution, including the main changes and their purpose] 4 | 5 | ### Additional Notes 6 | 7 | [If you have any additional comments or notes, please add them here] 8 | 9 | ### Screenshots 10 | 11 | [Optional: Include screenshots that help explain your PR] 12 | 13 | ### Contributor Agreement 14 | 15 | By submitting this Pull Request, I confirm that I have read and agree to the following terms: 16 | 17 | - I agree to contribute all code submitted in this PR to the open-source community edition licensed under GPLv3 and the proprietary official edition without compensation. 18 | - I grant the official edition development team the rights to freely use, modify, and distribute this code, including for commercial purposes. 19 | - I confirm that this code is my original work, or I have obtained the appropriate authorization from the copyright holder to submit this code under these terms. 20 | - I understand that the submitted code will be publicly released under the GPLv3 license, and may also be used in the proprietary official edition. 21 | 22 | **Please check the box below to confirm:** 23 | 24 | [ ] I have read and agree with the above statement. 25 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | requiredHeaders: 2 | - Prerequisites 3 | - Expected Behavior 4 | - Current Behavior 5 | - Possible Solution 6 | - Your Environment 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - discussion 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v22.7.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "semi": false, 6 | "overrides": [ 7 | { 8 | "files": [ 9 | ".prettierrc", 10 | ".eslintrc" 11 | ], 12 | "options": { 13 | "parser": "json" 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record 2 | 3 | declare module '*.svg' { 4 | import React = require('react') 5 | 6 | export const ReactComponent: React.FC> 7 | 8 | const content: string 9 | export default content 10 | } 11 | 12 | declare module '*.png' { 13 | const content: string 14 | export default content 15 | } 16 | 17 | declare module '*.jpg' { 18 | const content: string 19 | export default content 20 | } 21 | 22 | declare module '*.scss' { 23 | const content: Styles 24 | export default content 25 | } 26 | 27 | declare module '*.sass' { 28 | const content: Styles 29 | export default content 30 | } 31 | 32 | declare module '*.css' { 33 | const content: Styles 34 | export default content 35 | } 36 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icon-1024.png -------------------------------------------------------------------------------- /assets/icon-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icon-raw.png -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icon.png -------------------------------------------------------------------------------- /assets/iconTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/iconTemplate.png -------------------------------------------------------------------------------- /assets/iconTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/iconTemplate@2x.png -------------------------------------------------------------------------------- /assets/iconTemplateRaw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/iconTemplateRaw.png -------------------------------------------------------------------------------- /assets/iconTemplateRawPreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/iconTemplateRawPreview.png -------------------------------------------------------------------------------- /assets/icon_pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icon_pro.png -------------------------------------------------------------------------------- /assets/icon_pro2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icon_pro2.png -------------------------------------------------------------------------------- /assets/icon_pro_plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icon_pro_plus.png -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/assets/icons/96x96.png -------------------------------------------------------------------------------- /doc/FAQ-CN.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 |

4 | English | 中文 5 |

6 | 7 | 这里列举了一些最常见的问题和解决方案。如果你依然没有找到答案,也可以提交一个 [Issue](https://github.com/Bin-Huang/chatbox/issues/new/choose)。 8 | 9 | ### 1001 10 | 11 | #### 消息发送失败,提示 `Failed to fetch`? 12 | 13 | 这是因为 Chatbox 无法连接到你设置的 AI 模型服务器,请检查你当前的网络环境,确保可以正常连接到 AI 模型服务器。 14 | 15 | 对于 OpenAI API 的用户,如果你选择了 OpenAI API 作为 AI 模型提供方(即设置页的 AI Provider 中选择了 `OpenAI API`),那么一般是 Chatbox 无法访问设置的 `API HOST`。在默认设置下,Chatbox 会使用 `https://api.openai.com` 作为 API HOST,请确保你的当前网络可以访问这个服务。注意,在某些国家和地区是无法直接访问的。 16 | 17 | ### 1002 18 | 19 | #### 以前用的好好的,突然报错 `{"error":{"message":"You exceeded your current quota, please check your plan and billing details.`? 20 | 21 | 如果你以前使用一切正常,某天之后突然无法使用过,并且每次发送消息都报错: 22 | 23 | ``` 24 | {"error":{"message":"You exceeded your current quota, please check your plan and billing details.","type":"insufficient_quota","param":null,"code":null}} 25 | ``` 26 | 27 | 请注意,这个问题和 Chatbox 没有任何关系。这个情况中往往是因为你正在使用自己的 OpenAI API 账户,而你账户中的免费额度已经全部用完或过期了(一般都是因为过期导致的)。你需要自行登录 OpenAI 账户的控制台,绑定一张海外信用卡才能继续使用。OpenAI API 账户对信用卡有很多要求,如果你的信用卡不符合要求,那么你需要自行解决(非常折腾)。 28 | 29 | **更推荐使用 `Chatbox AI`:** 如果你不想折腾这些问题,也可以使用 Chatbox 内置的 `Chatbox AI` 服务。这个服务可以让你无需折腾、什么都不用管、轻松使用 AI 能力。前往配置页,将 AI Provider 设置为 `Chatbox AI`,你将看到相应的设置。 30 | 31 | ### 1003 32 | 33 | #### 无法使用 GPT-4? 34 | 35 | 如果你选择 GPT-4,然后发送消息时得到类似的报错: 36 | 37 | ``` 38 | {"error":{"message":"The model: gpt-4-32k does not exist","type":"invalid_request_error","param":null,"code":"model_not_found"}} 39 | ``` 40 | 41 | 这个情况往往是因为你正在使用自己的 OpenAI 账户,你在模型中选择了 GPT-4,但 OpenAI API 账户不支持 GPT-4。截止到 2023 年 07 月 04 日,所有 OpenAI API 账户都需要向 OpenAI 填写申请后才能使用 GPT-4 模型。这里是申请链接: https://openai.com/waitlist/gpt-4-api 。请注意,即使你是 ChatGPT Plus 用户,你也需要申请后才能使用 GPT-4 的 API 模型。 42 | -------------------------------------------------------------------------------- /doc/statics/android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/android.png -------------------------------------------------------------------------------- /doc/statics/app_store.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/app_store.webp -------------------------------------------------------------------------------- /doc/statics/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/demo.png -------------------------------------------------------------------------------- /doc/statics/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/demo2.png -------------------------------------------------------------------------------- /doc/statics/demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/demo3.gif -------------------------------------------------------------------------------- /doc/statics/demo_desktop_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/demo_desktop_1.jpg -------------------------------------------------------------------------------- /doc/statics/demo_desktop_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/demo_desktop_2.jpg -------------------------------------------------------------------------------- /doc/statics/demo_desktop_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/demo_desktop_3.jpg -------------------------------------------------------------------------------- /doc/statics/google_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/google_play.png -------------------------------------------------------------------------------- /doc/statics/google_play.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/google_play.webp -------------------------------------------------------------------------------- /doc/statics/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/icon.png -------------------------------------------------------------------------------- /doc/statics/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/linux.png -------------------------------------------------------------------------------- /doc/statics/mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/mac.png -------------------------------------------------------------------------------- /doc/statics/snapshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/snapshot2.png -------------------------------------------------------------------------------- /doc/statics/snapshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/snapshot4.png -------------------------------------------------------------------------------- /doc/statics/snapshot_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/snapshot_dark.png -------------------------------------------------------------------------------- /doc/statics/snapshot_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/snapshot_light.png -------------------------------------------------------------------------------- /doc/statics/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/doc/statics/windows.png -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | productName: Chatbox 2 | appId: xyz.chatboxapp.app 3 | asar: true 4 | asarUnpack: "**\\*.{node,dll}" 5 | files: 6 | - dist 7 | - node_modules 8 | - package.json 9 | 10 | afterSign: .erb/scripts/notarize.js 11 | 12 | # releaseInfo: 13 | # releaseNotes: See the changelog for details 14 | 15 | mac: 16 | notarize: false 17 | category: public.app-category.developer-tools 18 | target: 19 | target: default 20 | arch: 21 | - arm64 22 | - x64 23 | type: distribution 24 | hardenedRuntime: true 25 | entitlements: assets/entitlements.mac.plist 26 | entitlementsInherit: assets/entitlements.mac.plist 27 | gatekeeperAssess: false 28 | 29 | dmg: 30 | contents: 31 | - x: 130 32 | y: 220 33 | - x: 410 34 | y: 220 35 | type: link 36 | path: /Applications 37 | 38 | win: 39 | target: 40 | - target: nsis 41 | arch: 42 | - x64 43 | - arm64 44 | verifyUpdateCodeSignature: false 45 | artifactName: ${productName}-${version}-Setup.${ext} 46 | sign: ./custom_win_sign.js 47 | signingHashAlgorithms: 48 | - sha256 49 | 50 | nsis: 51 | oneClick: false 52 | allowToChangeInstallationDirectory: true 53 | 54 | linux: 55 | target: 56 | - target: AppImage 57 | arch: 58 | - x64 59 | - arm64 60 | - target: deb 61 | arch: 62 | - x64 63 | - arm64 64 | category: Development 65 | artifactName: ${productName}-${version}-${arch}.${ext} 66 | 67 | directories: 68 | app: release/app 69 | buildResources: assets 70 | output: release/build 71 | 72 | extraResources: 73 | - ./assets/** 74 | 75 | publish: 76 | - provider: s3 77 | bucket: chatbox 78 | endpoint: https://208624959c9d215edea0720162a740c1.r2.cloudflarestorage.com 79 | path: /releases 80 | channel: ${env.UPDATE_CHANNEL} -------------------------------------------------------------------------------- /icons/icon-128.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/icons/icon-128.webp -------------------------------------------------------------------------------- /icons/icon-192.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/icons/icon-192.webp -------------------------------------------------------------------------------- /icons/icon-256.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/icons/icon-256.webp -------------------------------------------------------------------------------- /icons/icon-48.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/icons/icon-48.webp -------------------------------------------------------------------------------- /icons/icon-512.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/icons/icon-512.webp -------------------------------------------------------------------------------- /icons/icon-72.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/icons/icon-72.webp -------------------------------------------------------------------------------- /icons/icon-96.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/icons/icon-96.webp -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | // jest.config.ts 2 | import { Config } from '@jest/types'; 3 | 4 | const config: Config.InitialOptions = { 5 | moduleDirectories: [ 6 | "node_modules", 7 | "release/app/node_modules", 8 | "src", 9 | ], 10 | moduleFileExtensions: [ 11 | "js", 12 | "jsx", 13 | "ts", 14 | "tsx", 15 | "json", 16 | ], 17 | moduleNameMapper: { 18 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/.erb/mocks/fileMock.js", 19 | "\\.(css|less|sass|scss)$": "identity-obj-proxy", 20 | "^@/(.*)$": "/src/renderer/$1", 21 | "^src/(.*)$": "/src/$1", 22 | }, 23 | setupFiles: [ 24 | "./.erb/scripts/check-build-exists.ts", 25 | ], 26 | testEnvironment: "jsdom", 27 | testEnvironmentOptions: { 28 | url: "http://localhost/", 29 | }, 30 | testPathIgnorePatterns: [ 31 | "release/app/dist", 32 | ".erb/dll", 33 | ], 34 | transform: { 35 | "\\.(ts|tsx|js|jsx)$": "ts-jest", 36 | }, 37 | }; 38 | 39 | export default config; 40 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | 'postcss-preset-mantine': {}, 7 | 'postcss-simple-vars': { 8 | variables: { 9 | 'mantine-breakpoint-xs': '36em', 10 | 'mantine-breakpoint-sm': '48em', 11 | 'mantine-breakpoint-md': '62em', 12 | 'mantine-breakpoint-lg': '75em', 13 | 'mantine-breakpoint-xl': '88em', 14 | }, 15 | }, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /release/app/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xyz.chatboxapp.ce", 3 | "version": "1.13.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "xyz.chatboxapp.ce", 9 | "version": "1.13.1", 10 | "hasInstallScript": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xyz.chatboxapp.ce", 3 | "productName": "xyz.chatboxapp.ce", 4 | "version": "1.13.1", 5 | "description": "A desktop client for multiple cutting-edge AI models", 6 | "author": { 7 | "name": "Mediocre Company", 8 | "email": "hi@chatboxai.com", 9 | "url": "https://github.com/chatboxai" 10 | }, 11 | "main": "./dist/main/main.js", 12 | "scripts": { 13 | "rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 14 | "postinstall": "npm run rebuild && npm run link-modules", 15 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts" 16 | }, 17 | "dependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /resources/icon-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/resources/icon-background.png -------------------------------------------------------------------------------- /resources/icon-foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/resources/icon-foreground.png -------------------------------------------------------------------------------- /resources/icon-only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/resources/icon-only.png -------------------------------------------------------------------------------- /resources/splash-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/resources/splash-dark.png -------------------------------------------------------------------------------- /resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/resources/splash.png -------------------------------------------------------------------------------- /src/__tests__/App.test.tsx.bk: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import { render } from '@testing-library/react' 3 | import App from '../renderer/App' 4 | 5 | describe('App', () => { 6 | it('should render', () => { 7 | expect(render()).toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/main/analystic-node.ts: -------------------------------------------------------------------------------- 1 | import * as store from './store-node' 2 | import { app } from 'electron' 3 | import { ofetch } from 'ofetch' 4 | 5 | // Measurement Protocol 参考文档 6 | // https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=zh-cn&client_type=gtag 7 | // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag&hl=zh-cn#required_parameters 8 | 9 | // 事件名、参数名,必须是字母、数字、下划线的组合 10 | 11 | const measurement_id = `G-B365F44W6E` 12 | const api_secret = `pRnsvLo-REWLVzV_PbKvWg` 13 | 14 | export async function event(name: string, params: any = {}) { 15 | const clientId = store.getConfig().uuid 16 | const res = await ofetch( 17 | `https://www.google-analytics.com/mp/collect?measurement_id=${measurement_id}&api_secret=${api_secret}`, 18 | { 19 | method: 'POST', 20 | body: { 21 | user_id: clientId, 22 | client_id: clientId, 23 | events: [ 24 | { 25 | name: name, 26 | params: { 27 | app_name: 'chatbox', 28 | app_version: app.getVersion(), 29 | chatbox_platform_type: 'desktop', 30 | chatbox_platform: 'desktop', 31 | app_platform: process.platform, 32 | ...params, 33 | }, 34 | }, 35 | ], 36 | }, 37 | } 38 | ) 39 | return res 40 | } 41 | -------------------------------------------------------------------------------- /src/main/autoLauncher.ts: -------------------------------------------------------------------------------- 1 | import AutoLaunch from 'auto-launch' 2 | import { getSettings } from './store-node' 3 | 4 | // 开机自启动 5 | let _autoLaunch: AutoLaunch | null = null 6 | 7 | export function get() { 8 | if (!_autoLaunch) { 9 | _autoLaunch = new AutoLaunch({ name: 'Chatbox' }) 10 | } 11 | return _autoLaunch 12 | } 13 | 14 | export async function sync() { 15 | const autoLaunch = get() 16 | const settings = getSettings() 17 | const isEnabled = await autoLaunch.isEnabled() 18 | if (!isEnabled && settings.autoLaunch) { 19 | await autoLaunch.enable() 20 | return 21 | } 22 | if (isEnabled && !settings.autoLaunch) { 23 | await autoLaunch.disable() 24 | return 25 | } 26 | } 27 | 28 | export async function ensure(enable: boolean) { 29 | const autoLaunch = get() 30 | const isEnabled = await autoLaunch.isEnabled() 31 | if (!isEnabled && enable) { 32 | await autoLaunch.enable() 33 | return 34 | } 35 | if (isEnabled && !enable) { 36 | await autoLaunch.disable() 37 | return 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/file-parser.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import officeParser from 'officeparser' 3 | import { isOfficeFilePath } from '../shared/file-extensions' 4 | import { getLogger } from './util' 5 | 6 | const log = getLogger('file-parser') 7 | 8 | export async function parseFile(filePath: string) { 9 | if (isOfficeFilePath(filePath)) { 10 | try { 11 | const data = await officeParser.parseOfficeAsync(filePath) 12 | return data 13 | } catch (error) { 14 | log.error(error) 15 | throw error 16 | } 17 | } 18 | const data = await fs.readFile(filePath, 'utf8') 19 | return data 20 | } 21 | -------------------------------------------------------------------------------- /src/main/locales.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | 3 | export default class Locale { 4 | locale: string = 'en' 5 | 6 | constructor() { 7 | try { 8 | this.locale = app.getLocale() 9 | } catch (e) { 10 | console.log(e) 11 | } 12 | } 13 | 14 | isCN(): boolean { 15 | return this.locale.startsWith('zh') 16 | } 17 | 18 | t(key: TranslationKey): string { 19 | return translations[key][this.isCN() ? 'zh' : 'en'] 20 | } 21 | } 22 | 23 | type TranslationKey = keyof typeof translations 24 | 25 | const translations = { 26 | 'Show/Hide': { 27 | en: 'Show/Hide', 28 | zh: '显示/隐藏', 29 | }, 30 | Exit: { 31 | en: 'Exit', 32 | zh: '退出', 33 | }, 34 | New_Version: { 35 | en: 'New Version', 36 | zh: '新版本', 37 | }, 38 | Restart: { 39 | en: 'Restart', 40 | zh: '重启', 41 | }, 42 | Later: { 43 | en: 'Later', 44 | zh: '稍后', 45 | }, 46 | App_Update: { 47 | en: 'App Update', 48 | zh: '应用更新', 49 | }, 50 | New_Version_Downloaded: { 51 | en: 'New version has been downloaded, restart the application to apply the update.', 52 | zh: '新版本已经下载好,重启应用以应用更新。', 53 | }, 54 | Copy: { 55 | en: 'Copy', 56 | zh: '复制', 57 | }, 58 | Cut: { 59 | en: 'Cut', 60 | zh: '剪切', 61 | }, 62 | Paste: { 63 | en: 'Paste', 64 | zh: '粘贴', 65 | }, 66 | PasteAsPlainText: { 67 | en: 'Paste as Plain Text', 68 | zh: '粘贴为文本', 69 | }, 70 | ReplaceWith: { 71 | en: 'Replace with', 72 | zh: '替换成', 73 | }, 74 | ResetZoom: { 75 | en: 'Reset Zoom', 76 | zh: '重置缩放', 77 | }, 78 | ZoomIn: { 79 | en: 'Zoom In', 80 | zh: '放大', 81 | }, 82 | ZoomOut: { 83 | en: 'Zoom Out', 84 | zh: '缩小', 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | // Disable no-unused-vars, broken for spread args 2 | /* eslint no-unused-vars: off */ 3 | import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron' 4 | import { ElectronIPC } from 'src/shared/electron-types' 5 | 6 | // export type Channels = 'ipc-example'; 7 | 8 | const electronHandler: ElectronIPC = { 9 | // ipcRenderer: { 10 | // sendMessage(channel: Channels, ...args: unknown[]) { 11 | // ipcRenderer.send(channel, ...args); 12 | // }, 13 | // on(channel: Channels, func: (...args: unknown[]) => void) { 14 | // const subscription = ( 15 | // _event: IpcRendererEvent, 16 | // ...args: unknown[] 17 | // ) => func(...args); 18 | // ipcRenderer.on(channel, subscription); 19 | 20 | // return () => { 21 | // ipcRenderer.removeListener(channel, subscription); 22 | // }; 23 | // }, 24 | // once(channel: Channels, func: (...args: unknown[]) => void) { 25 | // ipcRenderer.once(channel, (_event, ...args) => func(...args)); 26 | // }, 27 | // }, 28 | invoke: ipcRenderer.invoke, 29 | onSystemThemeChange: (callback: () => void) => { 30 | ipcRenderer.on('system-theme-updated', callback) 31 | return () => ipcRenderer.off('system-theme-updated', callback) 32 | }, 33 | onWindowShow: (callback: () => void) => { 34 | ipcRenderer.on('window-show', callback) 35 | return () => ipcRenderer.off('window-show', callback) 36 | }, 37 | onUpdateDownloaded: (callback: () => void) => { 38 | ipcRenderer.on('update-downloaded', callback) 39 | return () => ipcRenderer.off('update-downloaded', callback) 40 | }, 41 | } 42 | 43 | contextBridge.exposeInMainWorld('electronAPI', electronHandler) 44 | -------------------------------------------------------------------------------- /src/main/proxy.ts: -------------------------------------------------------------------------------- 1 | import { session } from 'electron' 2 | import * as store from './store-node' 3 | 4 | export function init() { 5 | const { proxy } = store.getSettings() 6 | if (proxy) { 7 | ensure(proxy) 8 | } 9 | } 10 | 11 | export function ensure(proxy?: string) { 12 | if (proxy) { 13 | session.defaultSession.setProxy({ proxyRules: proxy }) 14 | } else { 15 | session.defaultSession.setProxy({}) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/readability.ts: -------------------------------------------------------------------------------- 1 | // import { Readability } from '@mozilla/readability' 2 | // import { parseHTML } from 'linkedom' 3 | // import { fetch } from 'ofetch' 4 | // import { sliceTextWithEllipsis } from './util' 5 | 6 | // // linkedom 只能在 Node.js 环境,且在网页中 fetch 其他 URL 很容易出现 CORS 问题 7 | 8 | // export async function readability(url: string, options: { maxLength?: number } = {}) { 9 | // const userAgents = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36` 10 | // const documentString = await fetch(url, { 11 | // headers: { 12 | // 'User-Agent': userAgents, 13 | // }, 14 | // }).then((res) => res.text()) 15 | // const { document } = parseHTML(documentString) 16 | // const reader = new Readability(document, {}) 17 | // const title = document.querySelector('title')?.textContent || undefined 18 | // const result = reader.parse() 19 | 20 | // const ret = { 21 | // title, 22 | // text: (result?.textContent || '').trim(), 23 | // } 24 | // if (options.maxLength) { 25 | // ret.text = sliceTextWithEllipsis(ret.text, options.maxLength) 26 | // } 27 | // return ret 28 | // } 29 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log/main' 2 | import path from 'path' 3 | import { URL } from 'url' 4 | 5 | export function resolveHtmlPath(htmlFileName: string) { 6 | if (process.env.NODE_ENV === 'development') { 7 | const port = process.env.PORT || 1212 8 | const url = new URL(`http://localhost:${port}`) 9 | url.pathname = htmlFileName 10 | return url.href 11 | } 12 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}` 13 | } 14 | 15 | export function sliceTextWithEllipsis(text: string, maxLength: number) { 16 | if (text.length <= maxLength) { 17 | return text 18 | } 19 | // 这里添加了一些根据文本的随机性,避免内容被截断 20 | const headLength = Math.floor(maxLength * 0.4) + Math.floor(text.length * 0.1) 21 | const tailLength = Math.floor(maxLength * 0.5) 22 | const head = text.slice(0, headLength) 23 | const tail = text.slice(-tailLength) 24 | 25 | return head + tail 26 | } 27 | 28 | // 初始化后,dev 模式可以收集到 renderer 层日志,但 electron 打包后无法正常工作 29 | // log.initialize() 30 | 31 | export function getLogger(logId: string) { 32 | const logger = log.create({ logId }) 33 | logger.transports.console.format = '{h}:{i}:{s}.{ms} › [{logId}] › {text}' 34 | logger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [{logId}] {text}' 35 | return logger 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/components/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles' 2 | import MuiAccordionDetails from '@mui/material/AccordionDetails' 3 | import MuiAccordion, { AccordionProps } from '@mui/material/Accordion' 4 | import MuiAccordionSummary, { AccordionSummaryProps } from '@mui/material/AccordionSummary' 5 | import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp' 6 | 7 | export const Accordion = styled((props: AccordionProps) => ( 8 | 9 | ))(({ theme }) => ({ 10 | border: `1px solid ${theme.palette.divider}`, 11 | '&:not(:last-child)': { 12 | // borderBottom: 0, 13 | }, 14 | '&:before': { 15 | display: 'none', 16 | }, 17 | })) 18 | 19 | export const AccordionSummary = styled((props: AccordionSummaryProps) => ( 20 | } {...props} /> 21 | ))(({ theme }) => ({ 22 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, .05)' : 'rgba(0, 0, 0, .01)', 23 | flexDirection: 'row-reverse', 24 | '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { 25 | transform: 'rotate(90deg)', 26 | }, 27 | '& .MuiAccordionSummary-content': { 28 | marginLeft: theme.spacing(1), 29 | }, 30 | })) 31 | 32 | export const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ 33 | padding: theme.spacing(2), 34 | border: '1px solid rgba(0, 0, 0, .125)', 35 | })) 36 | -------------------------------------------------------------------------------- /src/renderer/components/CustomProviderIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from '@mantine/core' 2 | import { FC } from 'react' 3 | 4 | export type CustomProviderIconProps = { 5 | providerId: string 6 | providerName: string 7 | size?: number 8 | } 9 | 10 | const BG_COLORS = [ 11 | '#1ABC9C', // 活力绿 12 | '#3498DB', // 明亮蓝 13 | '#9B59B6', // 紫色 14 | '#E67E22', // 橙色 15 | '#E74C3C', // 鲜红 16 | '#2ECC71', // 草绿 17 | '#34495E', // 深蓝灰 18 | '#F1C40F', // 明黄 19 | '#F39C12', // 橙黄 20 | '#16A085', // 墨绿 21 | '#2980B9', // 深蓝 22 | '#8E44AD', // 深紫 23 | '#2C3E50', // 暗靛 24 | '#C0392B', // 深红 25 | '#27AE60', // 洋绿 26 | '#7F8C8D', // 高级灰 27 | ] 28 | 29 | const DEFAULT_SIZE = 32 30 | 31 | export const CustomProviderIcon: FC = ({ providerId, providerName, size = DEFAULT_SIZE }) => { 32 | const char = providerName.slice(0, 1).toUpperCase() || 'X' 33 | const color = BG_COLORS[providerId.split('').reduce((sum, cur) => sum + cur.charCodeAt(0), 0) % BG_COLORS.length] 34 | const textScale = size / DEFAULT_SIZE 35 | return ( 36 | 37 | 38 | {char} 39 | 40 | 41 | ) 42 | } 43 | 44 | export default CustomProviderIcon 45 | -------------------------------------------------------------------------------- /src/renderer/components/ExitFullscreenButton.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import platform from '../platform' 3 | import { debounce } from 'lodash' 4 | 5 | /** 6 | * 为 Windows 桌面用户准备的全屏退出按钮。一些用户会按 F11 强制进入全屏,但是却不知道怎么退出去。 7 | * @returns 8 | */ 9 | export default function ExitFullscreenButton() { 10 | const [isFullscreen, setIsFullscreen] = useState(false) 11 | useEffect(() => { 12 | const checkFullscreen = async () => { 13 | const isFullscreen = await platform.isFullscreen() 14 | setIsFullscreen(isFullscreen) 15 | } 16 | // 初始检查 17 | checkFullscreen() 18 | // 监听窗口变化事件 19 | const handleResize = debounce(() => { 20 | checkFullscreen() 21 | }, 1 * 1000) 22 | window.addEventListener('resize', handleResize) 23 | return () => { 24 | window.removeEventListener('resize', handleResize) 25 | } 26 | }, []) 27 | const onClick = () => { 28 | platform.setFullscreen(false) 29 | } 30 | if (!isFullscreen) { 31 | return null 32 | } 33 | return ( 34 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/components/Image.tsx: -------------------------------------------------------------------------------- 1 | import storage from '@/storage' 2 | import React, { useEffect, useState } from 'react' 3 | import CircularProgressIcon from '@mui/material/CircularProgress' 4 | import BrokenImageOutlinedIcon from '@mui/icons-material/BrokenImageOutlined' 5 | 6 | export function ImageInStorage(props: { 7 | storageKey: string 8 | className?: string 9 | onClick?: (e: React.MouseEvent) => void 10 | }) { 11 | // false 意味着不存在 12 | const [base64, setPic] = useState('') 13 | useEffect(() => { 14 | storage.getBlob(props.storageKey).then((blob) => { 15 | if (blob) { 16 | setPic(blob) 17 | } else { 18 | setPic(false) 19 | } 20 | }) 21 | }, [props.storageKey]) 22 | if (!base64) { 23 | return ( 24 |
25 |
26 | {base64 === false ? ( 27 | 28 | ) : ( 29 | 30 | )} 31 |
32 |
33 | ) 34 | } 35 | const picBase64 = base64.startsWith('data:image/') ? base64 : `data:image/png;base64,${base64}` 36 | return 37 | } 38 | 39 | export function Img(props: { 40 | src: string 41 | className?: string 42 | onClick?: (e: React.MouseEvent) => void 43 | }) { 44 | return 45 | } 46 | 47 | export function handleImageInputAndSave(file: File, key: string, updateKey?: (key: string) => void) { 48 | if (file.type.startsWith('image/')) { 49 | const reader = new FileReader() 50 | reader.onload = async (e) => { 51 | if (e.target && e.target.result) { 52 | const base64 = e.target.result as string 53 | await storage.setBlob(key, base64) 54 | if (updateKey) { 55 | updateKey(key) 56 | } 57 | } 58 | } 59 | reader.readAsDataURL(file) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/components/ImageCountSlider.tsx: -------------------------------------------------------------------------------- 1 | import { TextField, Slider, Typography, Box } from '@mui/material' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | export interface Props { 5 | value: number 6 | onChange(value: number): void 7 | className?: string 8 | } 9 | 10 | export default function ImageCountSlider(props: Props) { 11 | const { t } = useTranslation() 12 | return ( 13 | 14 | 15 | {t('Number of Images per Reply')} 16 | 17 | 24 | 25 | { 28 | const v = Array.isArray(value) ? value[0] : value 29 | props.onChange(v) 30 | }} 31 | aria-labelledby="discrete-slider" 32 | valueLabelDisplay="auto" 33 | step={1} 34 | min={1} 35 | max={10} 36 | marks 37 | /> 38 | 39 | { 43 | const s = event.target.value.trim() 44 | const v = parseInt(s) 45 | if (isNaN(v)) { 46 | return 47 | } 48 | if (v < 0) { 49 | return 50 | } 51 | props.onChange(v) 52 | }} 53 | type="text" 54 | size="small" 55 | variant="outlined" 56 | /> 57 | 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/renderer/components/ImageModelSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useProviders } from '@/hooks/useProviders' 2 | import { Combobox, ComboboxProps, useCombobox } from '@mantine/core' 3 | import { FC, PropsWithChildren } from 'react' 4 | import { ModelProvider } from 'src/shared/types' 5 | 6 | export type ImageModelSelectProps = PropsWithChildren< 7 | { 8 | onSelect?: (provider: ModelProvider, model: string) => void 9 | } & ComboboxProps 10 | > 11 | 12 | export const ImageModelSelect: FC = ({ onSelect, children, ...comboboxProps }) => { 13 | const { providers } = useProviders() 14 | 15 | const avaliableProviders = providers.filter((p) => [ModelProvider.OpenAI, ModelProvider.Azure, ''].includes(p.id)) 16 | 17 | const combobox = useCombobox({ 18 | onDropdownClose: () => { 19 | combobox.resetSelectedOption() 20 | combobox.focusTarget() 21 | }, 22 | }) 23 | 24 | const handleOptionSubmit = (val: string) => { 25 | onSelect?.(val as any, 'DALL-E-3') 26 | combobox.closeDropdown() 27 | } 28 | 29 | return ( 30 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | {/* Chatbox AI (DALL-E-3) 作为默认选项 */} 47 | 48 | Chatbox AI (DALL-E-3) 49 | 50 | {avaliableProviders.map((p) => ( 51 | 52 | {p.name} (DALL-E-3) 53 | 54 | ))} 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export default ImageModelSelect 62 | -------------------------------------------------------------------------------- /src/renderer/components/LazySlider.tsx: -------------------------------------------------------------------------------- 1 | import { Slider, SliderProps } from '@mantine/core' 2 | import { FC, useCallback, useState } from 'react' 3 | 4 | export type LazySliderProps = SliderProps 5 | 6 | export const LazySlider: FC = ({ value, onChange, ...otherProps }) => { 7 | const [tempSliderValue, setTempSliderValue] = useState() 8 | 9 | const handleSliderChange = useCallback((v: number) => { 10 | setTempSliderValue(v) 11 | }, []) 12 | const handleSliderChangeEnd = useCallback((v: number) => { 13 | setTempSliderValue(undefined) 14 | onChange?.(v) 15 | }, []) 16 | 17 | return ( 18 | 24 | ) 25 | } 26 | 27 | export default LazySlider 28 | -------------------------------------------------------------------------------- /src/renderer/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import platform from '@/platform' 2 | import { useTheme } from '@mui/material' 3 | 4 | export default function LinkTargetBlank(props: { 5 | children?: React.ReactNode | string 6 | href: string 7 | className?: string 8 | style?: React.CSSProperties 9 | }) { 10 | const theme = useTheme() 11 | const { children, href, className, style } = props 12 | return ( 13 | { 20 | event.stopPropagation() 21 | event.preventDefault() 22 | platform.openLink(href) 23 | }} 24 | href={href} 25 | target="_blank" 26 | > 27 | {children} 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/components/Mark.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useEffect, useRef } from 'react' 2 | import markjs from 'mark.js' 3 | 4 | export default function Mark(props: { children: string | ReactElement; marks: string[] }) { 5 | const { children, marks } = props 6 | const ref = useRef(null) 7 | useEffect(() => { 8 | if (!ref.current) { 9 | return 10 | } 11 | const markInstance = new markjs(ref.current) 12 | markInstance.mark(marks) 13 | }, [children, ref.current, marks]) 14 | return
{children}
15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/components/MiniButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tooltip } from '@mui/material' 3 | import { cn } from '@/lib/utils' 4 | 5 | export default function MiniButton(props: { 6 | children: React.ReactNode 7 | onClick?: React.MouseEventHandler 8 | disabled?: boolean 9 | className?: string 10 | style?: React.CSSProperties 11 | tooltipTitle?: React.ReactNode 12 | tooltipPlacement?: 13 | | 'top' 14 | | 'bottom' 15 | | 'left' 16 | | 'right' 17 | | 'bottom-end' 18 | | 'bottom-start' 19 | | 'left-end' 20 | | 'left-start' 21 | | 'right-end' 22 | | 'right-start' 23 | | 'top-end' 24 | | 'top-start' 25 | }) { 26 | const { onClick, disabled, className, style, tooltipTitle, tooltipPlacement, children } = props 27 | const button = ( 28 | 42 | ) 43 | if (!tooltipTitle) { 44 | return button 45 | } 46 | return ( 47 | 48 | {button} 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/components/PasswordTextField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TextField, InputAdornment, IconButton } from '@mui/material' 3 | import Visibility from '@mui/icons-material/Visibility' 4 | import VisibilityOff from '@mui/icons-material/VisibilityOff' 5 | import { useIsSmallScreen } from '@/hooks/useScreenChange' 6 | 7 | export default function PasswordTextField(props: { 8 | label: string 9 | value: string 10 | setValue: (value: string) => void 11 | placeholder?: string 12 | disabled?: boolean 13 | helperText?: React.ReactNode 14 | }) { 15 | const isSmallScreen = useIsSmallScreen() 16 | const [showPassword, setShowPassword] = React.useState(false) 17 | const handleClickShowPassword = () => setShowPassword((show) => !show) 18 | const handleMouseDownPassword = (event: React.MouseEvent) => { 19 | event.preventDefault() 20 | } 21 | return ( 22 | props.setValue(e.target.value.trim())} 33 | InputProps={{ 34 | endAdornment: ( 35 | 36 | 41 | {showPassword ? : } 42 | 43 | 44 | ), 45 | }} 46 | helperText={props.helperText} 47 | /> 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/renderer/components/SimpleSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Select, MenuItem, FormControl, InputLabel } from '@mui/material' 2 | import * as React from 'react' 3 | 4 | export interface Props { 5 | label: string | React.ReactNode 6 | value: T 7 | options: { value: T; label: React.ReactNode; style?: React.CSSProperties }[] 8 | onChange: (value: T) => void 9 | className?: string 10 | fullWidth?: boolean 11 | size?: 'small' | 'medium' 12 | style?: React.CSSProperties 13 | } 14 | 15 | export default function SimpleSelect(props: Props) { 16 | const { fullWidth = true, size, style } = props 17 | return ( 18 | 26 | {props.label} 27 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/components/SortableItem.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/components/SortableItem.tsx -------------------------------------------------------------------------------- /src/renderer/components/SponsorChip.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Chip } from '@mui/material' 3 | import { SponsorAd } from '../../shared/types' 4 | import CampaignOutlinedIcon from '@mui/icons-material/CampaignOutlined' 5 | import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined' 6 | import platform from '../platform' 7 | import { useAtomValue } from 'jotai' 8 | import { currentSessionIdAtom } from '@/stores/atoms' 9 | 10 | export default function SponsorChip(props: {}) { 11 | const currrentSessionId = useAtomValue(currentSessionIdAtom) 12 | const [showSponsorAD, setShowSponsorAD] = useState(true) 13 | const [sponsorAD, setSponsorAD] = useState(null) 14 | // useEffect(() => { 15 | // ;(async () => { 16 | // const ad = await remote.getSponsorAd() 17 | // if (ad) { 18 | // setSponsorAD(ad) 19 | // } 20 | // })() 21 | // }, [currrentSessionId]) 22 | if (!showSponsorAD || !sponsorAD) { 23 | return <> 24 | } 25 | return ( 26 | } 40 | deleteIcon={} 41 | onDelete={() => setShowSponsorAD(false)} 42 | onClick={() => platform.openLink(sponsorAD.url)} 43 | label={sponsorAD.text} 44 | /> 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/components/StyledMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Menu, MenuProps } from '@mui/material' 2 | import 'katex/dist/katex.min.css' 3 | import { styled, alpha } from '@mui/material/styles' 4 | import { useAtomValue } from 'jotai' 5 | import * as atoms from '@/stores/atoms' 6 | 7 | const StyledMenu = styled((props: MenuProps) => { 8 | const language = useAtomValue(atoms.languageAtom) 9 | return ( 10 | 27 | ) 28 | })(({ theme }) => ({ 29 | '& .MuiPaper-root': { 30 | backgroundColor: theme.palette.mode === 'dark' ? theme.palette.grey[800] : theme.palette.grey[100], 31 | borderRadius: 6, 32 | marginTop: theme.spacing(1), 33 | minWidth: 140, 34 | color: theme.palette.mode === 'light' ? 'rgb(55, 65, 81)' : theme.palette.grey[300], 35 | boxShadow: 36 | 'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px', 37 | '& .MuiMenu-list': { 38 | padding: '0px 0', 39 | }, 40 | '& .MuiMenuItem-root': { 41 | padding: '8px', 42 | '& .MuiSvgIcon-root': { 43 | color: theme.palette.text.secondary, 44 | marginRight: theme.spacing(1.5), 45 | }, 46 | '&:active': { 47 | backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity), 48 | }, 49 | }, 50 | '& hr': { 51 | margin: '2px 0', 52 | }, 53 | }, 54 | '& .MuiPaper-root::-webkit-scrollbar': { 55 | width: '6px', 56 | }, 57 | '& .MuiPaper-root::-webkit-scrollbar-track': { 58 | background: 'transparent', 59 | }, 60 | '& .MuiPaper-root::-webkit-scrollbar-thumb': { 61 | backgroundColor: theme.palette.mode === 'light' ? '#0000001a' : '#ffffff1a', 62 | borderRadius: '4px', 63 | }, 64 | '& .MuiPaper-root::-webkit-scrollbar-thumb:hover': { 65 | backgroundColor: theme.palette.mode === 'light' ? '#0000003a' : '#ffffff3a', 66 | }, 67 | })) 68 | 69 | export default StyledMenu 70 | -------------------------------------------------------------------------------- /src/renderer/components/TemperatureSlider.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { TextField, Slider, Typography, Box } from '@mui/material' 3 | import { useTranslation } from 'react-i18next' 4 | 5 | export interface Props { 6 | value: number 7 | onChange(value: number): void 8 | className?: string 9 | } 10 | 11 | export default function TemperatureSlider(props: Props) { 12 | const { t } = useTranslation() 13 | const [input, setInput] = useState('0.70') 14 | useEffect(() => { 15 | setInput(`${props.value}`) 16 | }, [props.value]) 17 | const handleTemperatureChange = (event: Event, newValue: number | number[], activeThumb: number) => { 18 | if (typeof newValue === 'number') { 19 | props.onChange(newValue) 20 | } else { 21 | props.onChange(newValue[activeThumb]) 22 | } 23 | } 24 | const handleInputChange = (event: React.ChangeEvent) => { 25 | const value = event.target.value 26 | if (value === '' || value.endsWith('.')) { 27 | setInput(value) 28 | return 29 | } 30 | let num = parseFloat(value) 31 | if (isNaN(num)) { 32 | setInput(`${value}`) 33 | return 34 | } 35 | if (num < 0 || num > 1) { 36 | setInput(`${value}`) 37 | return 38 | } 39 | // 保留一位小数 40 | num = Math.round(num * 100) / 100 41 | setInput(num.toString()) 42 | props.onChange(num) 43 | } 44 | return ( 45 | 46 | 47 | {t('temperature')} 48 | 49 | 56 | 57 | 66 | 67 | 75 | 76 | 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /src/renderer/components/TextFieldReset.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TextField, Button } from '@mui/material' 3 | import { useTranslation } from 'react-i18next' 4 | 5 | export default function TextFieldReset( 6 | props: { 7 | defaultValue?: string 8 | value: string 9 | onValueChange: (value: string) => void 10 | } & Omit, 'defaultValue' | 'value' | 'onChange'> 11 | ) { 12 | const { t } = useTranslation() 13 | const { onValueChange, defaultValue = '', value, ...rest } = props 14 | const handleReset = () => onValueChange(defaultValue) 15 | const handleMouseDown = (event: React.MouseEvent) => { 16 | event.preventDefault() 17 | } 18 | return ( 19 | onValueChange(e.target.value)} 23 | InputProps={ 24 | defaultValue === props.value 25 | ? {} 26 | : { 27 | endAdornment: ( 28 | 31 | ), 32 | } 33 | } 34 | helperText={props.helperText} 35 | /> 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/components/Toasts.tsx: -------------------------------------------------------------------------------- 1 | import {} from 'react' 2 | import * as toastActions from '../stores/toastActions' 3 | import { Snackbar } from '@mui/material' 4 | import * as atoms from '../stores/atoms' 5 | import { useAtomValue } from 'jotai' 6 | 7 | function Toasts() { 8 | const toasts = useAtomValue(atoms.toastsAtom) 9 | return ( 10 | <> 11 | {toasts.map((toast) => ( 12 | toastActions.remove(toast.id)} 17 | message={toast.content} 18 | anchorOrigin={{ vertical: 'top', horizontal: 'right' }} 19 | autoHideDuration={5000} 20 | /> 21 | ))} 22 | 23 | ) 24 | } 25 | 26 | export default Toasts 27 | -------------------------------------------------------------------------------- /src/renderer/components/icons/ArrowRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler } from 'react' 2 | 3 | export default function ArrowRightIcon(props: { className?: string; onClick?: MouseEventHandler }) { 4 | const { className, onClick } = props 5 | return ( 6 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/components/icons/BrandGithub.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | function BrandGithub(props: React.SVGProps) { 4 | return ( 5 | 6 | 10 | 11 | ) 12 | } 13 | 14 | export default React.memo(BrandGithub) 15 | -------------------------------------------------------------------------------- /src/renderer/components/icons/BrandX.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler } from 'react' 2 | 3 | export default function BrandX(props: { className?: string; onClick?: MouseEventHandler }) { 4 | const { className, onClick } = props 5 | return ( 6 | 7 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/components/icons/FullscreenIcon.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler } from 'react' 2 | 3 | export default function FullscreenIcon(props: { className?: string; onClick?: MouseEventHandler }) { 4 | const { className, onClick } = props 5 | return ( 6 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/favicon.ico -------------------------------------------------------------------------------- /src/renderer/hooks/dom.ts: -------------------------------------------------------------------------------- 1 | // 有时候直接操作 DOM 依然是最方便、性能最好的方式,这里对 DOM 操作进行统一管理 2 | 3 | // ------ 消息输入框 ------ 4 | 5 | export const InputBoxID = 'input-box-2024-02-22' 6 | 7 | export function getInputBoxHeight(): number { 8 | const element = document.getElementById(InputBoxID) 9 | if (!element) { 10 | return 0 11 | } 12 | return element.clientHeight 13 | } 14 | 15 | // ------ 消息输入框表单(input) ------ 16 | 17 | export const messageInputID = 'message-input' 18 | 19 | export const focusMessageInput = () => { 20 | document.getElementById(messageInputID)?.focus() 21 | } 22 | 23 | // 将光标位置设置为文本末尾 24 | export function setMessageInputCursorToEnd() { 25 | const dom = document.getElementById(messageInputID) as HTMLTextAreaElement 26 | if (!dom) { 27 | return 28 | } 29 | dom.selectionStart = dom.selectionEnd = dom.value.length 30 | setTimeout(() => { 31 | dom.scrollTop = dom.scrollHeight 32 | }, 20) // 等待 React 状态更新 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/hooks/useChatboxAIModels.ts: -------------------------------------------------------------------------------- 1 | import { getModelManifest } from '@/packages/remote' 2 | import { languageAtom } from '@/stores/atoms' 3 | import { useQuery } from '@tanstack/react-query' 4 | import { useAtomValue } from 'jotai' 5 | import { useMemo } from 'react' 6 | import { ModelProvider, ProviderModelInfo } from 'src/shared/types' 7 | import { useProviderSettings } from './useSettings' 8 | 9 | const useChatboxAIModels = () => { 10 | const language = useAtomValue(languageAtom) 11 | const { providerSettings: chatboxAISettings, setProviderSettings } = useProviderSettings(ModelProvider.ChatboxAI) 12 | 13 | const { data, ...others } = useQuery({ 14 | queryKey: ['chatbox-ai-models', language], 15 | queryFn: async () => { 16 | const res = await getModelManifest({ 17 | aiProvider: ModelProvider.ChatboxAI, 18 | language, 19 | }) 20 | 21 | // ChatboxAI的设置中实际存的是excludedModels, models实际为空,这导致生成消息时无法拿到model的nickName,所以这里每次获取chatbox ai models之后就存在settings中 22 | setProviderSettings({ 23 | models: res.models.map((m) => ({ 24 | modelId: m.modelId, 25 | nickname: m.modelName, 26 | labels: m.labels, 27 | })), 28 | }) 29 | 30 | return res.models 31 | }, 32 | }) 33 | 34 | const allChatboxAIModels = useMemo( 35 | () => 36 | data?.map( 37 | (item) => 38 | ({ 39 | modelId: item.modelId, 40 | nickname: item.modelName, 41 | labels: item.labels, 42 | } as ProviderModelInfo) 43 | ) || [], 44 | [data] 45 | ) 46 | 47 | const chatboxAIModels = useMemo( 48 | () => allChatboxAIModels.filter((m) => !chatboxAISettings?.excludedModels?.includes(m.modelId)), 49 | [allChatboxAIModels, chatboxAISettings] 50 | ) 51 | 52 | return { allChatboxAIModels, chatboxAIModels, ...others } 53 | } 54 | 55 | export default useChatboxAIModels 56 | -------------------------------------------------------------------------------- /src/renderer/hooks/useCopilots.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import * as remote from '../packages/remote' 3 | import { CopilotDetail } from '../../shared/types' 4 | import { useAtom } from 'jotai' 5 | import { myCopilotsAtom } from '../stores/atoms' 6 | 7 | export function useMyCopilots() { 8 | const [copilots, setCopilots] = useAtom(myCopilotsAtom) 9 | 10 | const addOrUpdate = (target: CopilotDetail) => { 11 | setCopilots((copilots) => { 12 | let found = false 13 | const newCopilots = copilots.map((c) => { 14 | if (c.id === target.id) { 15 | found = true 16 | return target 17 | } 18 | return c 19 | }) 20 | if (!found) { 21 | newCopilots.push(target) 22 | } 23 | return newCopilots 24 | }) 25 | } 26 | 27 | const remove = (id: string) => { 28 | setCopilots((copilots) => copilots.filter((c) => c.id !== id)) 29 | } 30 | 31 | return { 32 | copilots, 33 | addOrUpdate, 34 | remove, 35 | } 36 | } 37 | 38 | export function useRemoteCopilots(lang: string, windowOpen: boolean) { 39 | const [copilots, _setCopilots] = useState([]) 40 | useEffect(() => { 41 | if (windowOpen) { 42 | remote.listCopilots(lang).then((copilots) => { 43 | _setCopilots(copilots) 44 | }) 45 | } 46 | }, [lang, windowOpen]) 47 | return { copilots } 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/hooks/useDefaultSystemLanguage.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultStore } from 'jotai' 2 | import { useEffect } from 'react' 3 | import { settingsAtom } from '../stores/atoms' 4 | import platform from '../platform' 5 | 6 | export function useSystemLanguageWhenInit() { 7 | useEffect(() => { 8 | // 通过定时器延迟启动,防止处理状态底层存储的异步加载前错误的初始数据 9 | setTimeout(() => { 10 | ;(async () => { 11 | const store = getDefaultStore() 12 | const settings = store.get(settingsAtom) 13 | if (!settings.languageInited) { 14 | let locale = await platform.getLocale() 15 | 16 | // 网页版暂时不自动更改简体中文,防止网址封禁 17 | if (platform.type === 'web') { 18 | if (locale === 'zh-Hans') { 19 | locale = 'en' 20 | } 21 | } 22 | 23 | settings.language = locale 24 | } 25 | settings.languageInited = true 26 | store.set(settingsAtom, { ...settings }) 27 | })() 28 | }, 2000) 29 | }, []) 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/hooks/useI18nEffect.ts: -------------------------------------------------------------------------------- 1 | import { useAtomValue } from 'jotai' 2 | import { useEffect } from 'react' 3 | import { useTranslation } from 'react-i18next' 4 | import { languageAtom } from '../stores/atoms' 5 | 6 | export function useI18nEffect() { 7 | const language = useAtomValue(languageAtom) 8 | const { i18n } = useTranslation() 9 | useEffect(() => { 10 | ;(async () => { 11 | i18n.changeLanguage(language) 12 | })() 13 | }, [language]) 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/hooks/useNeedRoomForWinControls.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import platform from '@/platform' 4 | import { useEffect } from 'react' 5 | 6 | function useNeedRoomForWinControls() { 7 | const [needRoomForMacWindowControls, setNeedRoomForMacWindowControls] = useState(false) 8 | const [needRoomForWindowsWindowControls, setNeedRoomForWindowsWindowControls] = useState(false) 9 | useEffect(() => { 10 | platform.getPlatform().then((platform) => { 11 | setNeedRoomForMacWindowControls(platform === 'darwin') 12 | setNeedRoomForWindowsWindowControls(platform === 'win32' || platform === 'linux') 13 | }) 14 | }, []) 15 | return { needRoomForMacWindowControls, needRoomForWindowsWindowControls } 16 | } 17 | 18 | export default useNeedRoomForWinControls 19 | -------------------------------------------------------------------------------- /src/renderer/hooks/useScreenChange.ts: -------------------------------------------------------------------------------- 1 | import { useTheme, useMediaQuery } from '@mui/material' 2 | import { useSetAtom } from 'jotai' 3 | import * as atoms from '../stores/atoms' 4 | import { useEffect } from 'react' 5 | 6 | export default function useScreenChange() { 7 | const setShowSidebar = useSetAtom(atoms.showSidebarAtom) 8 | const realIsSmallScreen = useIsSmallScreen() 9 | useEffect(() => { 10 | setShowSidebar(!realIsSmallScreen) 11 | }, [realIsSmallScreen]) 12 | } 13 | 14 | export function useIsSmallScreen() { 15 | const theme = useTheme() 16 | const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')) 17 | return isSmallScreen 18 | } 19 | 20 | export function useScreenDownToMD() { 21 | const theme = useTheme() 22 | return useMediaQuery(theme.breakpoints.down('md')) 23 | } 24 | 25 | export function useIsLargeScreen() { 26 | const theme = useTheme() 27 | return !useMediaQuery(theme.breakpoints.down('lg')) 28 | } 29 | 30 | export function useSidebarWidth() { 31 | const theme = useTheme() 32 | const sm = useMediaQuery(theme.breakpoints.up('sm')) 33 | const md = useMediaQuery(theme.breakpoints.up('md')) 34 | const lg = useMediaQuery(theme.breakpoints.up('lg')) 35 | const xl = useMediaQuery(theme.breakpoints.up('xl')) 36 | if (xl) { 37 | return 280 38 | } else if (lg) { 39 | return 240 40 | } else if (md) { 41 | return 220 42 | } else if (sm) { 43 | return 200 44 | } else { 45 | return 240 46 | } 47 | } 48 | 49 | export function useInputBoxHeight(): { min: number; max: number } { 50 | const theme = useTheme() 51 | const sm = useMediaQuery(theme.breakpoints.up('sm')) 52 | const md = useMediaQuery(theme.breakpoints.up('md')) 53 | // const lg = useMediaQuery(theme.breakpoints.up('lg')) 54 | const xl = useMediaQuery(theme.breakpoints.up('xl')) 55 | if (xl) { 56 | return { min: 96, max: 480 } 57 | } else if (md) { 58 | return { min: 72, max: 384 } 59 | } else if (sm) { 60 | return { min: 56, max: 288 } 61 | } else { 62 | return { min: 32, max: 192 } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/renderer/hooks/useSettings.ts: -------------------------------------------------------------------------------- 1 | import { ProviderSettings, Settings } from 'src/shared/types' 2 | import { useAtom } from 'jotai' 3 | import { useCallback } from 'react' 4 | import { settingsAtom } from '@/stores/atoms' 5 | 6 | export const useSettings = () => { 7 | const [settings, _setSettings] = useAtom(settingsAtom) 8 | 9 | const setSettings = useCallback((val: Partial) => { 10 | _setSettings((pre) => ({ 11 | ...pre, 12 | ...val, 13 | })) 14 | }, []) 15 | 16 | return { 17 | settings, 18 | setSettings, 19 | } 20 | } 21 | 22 | export const useProviderSettings = (providerId: string) => { 23 | const { settings, setSettings } = useSettings() 24 | 25 | const providerSettings = settings.providers?.[providerId] 26 | 27 | const setProviderSettings = (val: Partial) => { 28 | setSettings({ 29 | providers: { 30 | ...(settings.providers || {}), 31 | [providerId]: { 32 | ...(settings.providers?.[providerId] || {}), 33 | ...val, 34 | }, 35 | }, 36 | }) 37 | } 38 | 39 | return { 40 | providerSettings, 41 | setProviderSettings, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/renderer/hooks/useVersion.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react' 2 | import platform from '../platform' 3 | 4 | export default function useVersion() { 5 | const [version, _setVersion] = useState('') 6 | const updateCheckTimer = useRef() 7 | useEffect(() => { 8 | const handler = async () => { 9 | const version = await platform.getVersion() 10 | _setVersion(version) 11 | } 12 | handler() 13 | updateCheckTimer.current = setInterval(handler, 2 * 60 * 60 * 1000) 14 | return () => { 15 | if (updateCheckTimer.current) { 16 | clearInterval(updateCheckTimer.current) 17 | updateCheckTimer.current = undefined 18 | } 19 | } 20 | }, []) 21 | 22 | return { 23 | version, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | 4 | import en from './locales/en/translation.json' 5 | import zhHans from './locales/zh-Hans/translation.json' 6 | import zhHant from './locales/zh-Hant/translation.json' 7 | import ja from './locales/ja/translation.json' 8 | import ko from './locales/ko/translation.json' 9 | import ru from './locales/ru/translation.json' 10 | import de from './locales/de/translation.json' 11 | import fr from './locales/fr/translation.json' 12 | import ptPT from './locales/pt-PT/translation.json' 13 | import itIT from './locales/it-IT/translation.json' 14 | import es from './locales/es/translation.json' 15 | import ar from './locales/ar/translation.json' 16 | import sv from './locales/sv/translation.json' 17 | import nbNO from './locales/nb-NO/translation.json' 18 | 19 | import changelogZhHans from './changelogs/changelog_zh_Hans' 20 | import changelogZhHant from './changelogs/changelog_zh_Hant' 21 | import changelogEn from './changelogs/changelog_en' 22 | 23 | i18n.use(initReactI18next).init({ 24 | resources: { 25 | 'zh-Hans': { 26 | translation: zhHans, 27 | }, 28 | 'zh-Hant': { 29 | translation: zhHant, 30 | }, 31 | en: { 32 | translation: en, 33 | }, 34 | ja: { 35 | translation: ja, 36 | }, 37 | ko: { 38 | translation: ko, 39 | }, 40 | ru: { 41 | translation: ru, 42 | }, 43 | de: { 44 | translation: de, 45 | }, 46 | fr: { 47 | translation: fr, 48 | }, 49 | 'pt-PT': { 50 | translation: ptPT, 51 | }, 52 | es: { 53 | translation: es, 54 | }, 55 | ar: { 56 | translation: ar, 57 | }, 58 | 'it-IT': { 59 | translation: itIT, 60 | }, 61 | sv: { 62 | translation: sv, 63 | }, 64 | 'nb-NO': { 65 | translation: nbNO, 66 | }, 67 | }, 68 | fallbackLng: 'en', 69 | 70 | interpolation: { 71 | escapeValue: false, 72 | }, 73 | 74 | detection: { 75 | caches: [], 76 | }, 77 | }) 78 | 79 | export default i18n 80 | 81 | export function changelog() { 82 | switch (i18n.language) { 83 | case 'zh-Hans': 84 | return changelogZhHans 85 | case 'zh-Hant': 86 | return changelogZhHant 87 | case 'en': 88 | return changelogEn 89 | default: 90 | return changelogEn 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/renderer/i18n/locales.ts: -------------------------------------------------------------------------------- 1 | import { Language } from '../../shared/types' 2 | 3 | export const languageNameMap: Record = { 4 | en: 'English', 5 | 'zh-Hans': '简体中文', 6 | 'zh-Hant': '繁體中文', 7 | ja: '日本語', 8 | ko: '한국어', 9 | ru: 'Русский', // Russian 10 | de: 'Deutsch', // German 11 | fr: 'Français', // French 12 | 'pt-PT': 'Português', // Portuguese 13 | es: 'Español', // Spanish 14 | ar: 'العربية', // Arabic 15 | 'it-IT': 'Italiano', // Italian 16 | sv: 'Svenska', // Swedish 瑞典语 17 | 'nb-NO': 'Norsk', // Norwegian 挪威语 18 | } 19 | 20 | export const languages = Array.from(Object.keys(languageNameMap)) as Language[] 21 | -------------------------------------------------------------------------------- /src/renderer/i18n/parser.ts: -------------------------------------------------------------------------------- 1 | import { Language } from '../../shared/types' 2 | 3 | // 将 electron getLocale、浏览器的 navigator.language 返回的语言信息,转换为应用的 locale 4 | export function parseLocale(locale: string): Language { 5 | if ( 6 | locale === 'zh' || 7 | locale.startsWith('zh_CN') || 8 | locale.startsWith('zh-CN') || 9 | locale.startsWith('zh_Hans') || 10 | locale.startsWith('zh-Hans') 11 | ) { 12 | return 'zh-Hans' 13 | } 14 | if ( 15 | locale.startsWith('zh_HK') || 16 | locale.startsWith('zh-HK') || 17 | locale.startsWith('zh_TW') || 18 | locale.startsWith('zh-TW') || 19 | locale.startsWith('zh_Hant') || 20 | locale.startsWith('zh-Hant') 21 | ) { 22 | return 'zh-Hant' 23 | } 24 | if (locale.startsWith('ja')) { 25 | return 'ja' 26 | } 27 | if (locale.startsWith('ko')) { 28 | return 'ko' 29 | } 30 | if (locale.startsWith('ru')) { 31 | return 'ru' 32 | } 33 | if (locale.startsWith('de')) { 34 | return 'de' 35 | } 36 | if (locale.startsWith('fr')) { 37 | return 'fr' 38 | } 39 | if (locale.startsWith('pt')) { 40 | // 这两种语言都是葡萄牙语,但是区域不同,一些用词习惯也不同,以后可能需要区分 41 | // 葡萄牙(Portugal) - pt-PT 42 | // 巴西(Brazil) - pt-BR 43 | return 'pt-PT' 44 | } 45 | if (locale.startsWith('es')) { 46 | return 'es' 47 | } 48 | if (locale.startsWith('ar')) { 49 | return 'ar' 50 | } 51 | if (locale.startsWith('it')) { 52 | return 'it-IT' 53 | } 54 | if (locale.startsWith('sv')) { 55 | return 'sv' 56 | } 57 | if (locale.startsWith('nb')) { 58 | return 'nb-NO' 59 | } 60 | return 'en' 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/index.web.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Chatbox网页版 11 | 12 | 13 | 20 | 21 | 22 | 23 |
24 |
25 |

Chatbox

26 |

loading...

27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /src/renderer/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | import log from 'electron-log/renderer' 4 | import { getDefaultStore } from 'jotai' 5 | import { initLogAtom } from '@/stores/atoms/utilAtoms' 6 | import dayjs from 'dayjs' 7 | 8 | export function cn(...inputs: ClassValue[]) { 9 | return twMerge(clsx(inputs)) 10 | } 11 | 12 | export function parseJsonOrEmpty(json: string): any { 13 | try { 14 | return JSON.parse(json) 15 | } catch (e) { 16 | return {} 17 | } 18 | } 19 | 20 | export function getLogger(logId: string) { 21 | // const logger = log.create({ logId }) 22 | // logger.transports.console.format = '{h}:{i}:{s}.{ms} › [{logId}] › {text}' 23 | // return logger 24 | return { 25 | log(level: string, ...args: any[]) { 26 | const store = getDefaultStore() 27 | const now = dayjs().format('HH:mm:ss.SSS') 28 | store.set(initLogAtom, [...store.get(initLogAtom), `[${now}][${logId}] ${args.join(' ')}`]) 29 | }, 30 | info(...args: any[]) { 31 | this.log('info', ...args) 32 | }, 33 | error(...args: any[]) { 34 | this.log('error', ...args) 35 | }, 36 | debug(...args: any[]) { 37 | this.log('debug', ...args) 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/modals/ArtifactPreview.tsx: -------------------------------------------------------------------------------- 1 | import NiceModal, { muiDialogV5, useModal } from '@ebay/nice-modal-react' 2 | import { Button, Dialog, DialogContent, DialogActions, DialogTitle } from '@mui/material' 3 | import { useTranslation } from 'react-i18next' 4 | import { Artifact } from '@/components/Artifact' 5 | import { useState } from 'react' 6 | 7 | const ArtifactPreview = NiceModal.create(({ htmlCode }: { htmlCode: string }) => { 8 | const modal = useModal() 9 | const { t } = useTranslation() 10 | const [reloadSign, setReloadSign] = useState(0) 11 | const onReload = () => { 12 | setReloadSign(Math.random()) 13 | } 14 | const onClose = () => { 15 | modal.resolve() 16 | modal.hide() 17 | } 18 | 19 | return ( 20 | { 23 | modal.resolve() 24 | modal.hide() 25 | }} 26 | fullWidth 27 | maxWidth="md" 28 | classes={{ paper: 'h-4/5' }} 29 | > 30 | {t('Preview')} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | }) 41 | 42 | export default ArtifactPreview 43 | -------------------------------------------------------------------------------- /src/renderer/modals/AttachLink.tsx: -------------------------------------------------------------------------------- 1 | import NiceModal, { muiDialogV5, useModal } from '@ebay/nice-modal-react' 2 | import { TextField, Button, Dialog, DialogContent, DialogActions, DialogTitle } from '@mui/material' 3 | import { useTranslation } from 'react-i18next' 4 | import { useState } from 'react' 5 | import _ from 'lodash' 6 | 7 | const AttachLink = NiceModal.create(() => { 8 | const modal = useModal() 9 | const { t } = useTranslation() 10 | const [input, setInput] = useState('') 11 | const onClose = () => { 12 | modal.resolve([]) 13 | modal.hide() 14 | } 15 | const onSubmit = () => { 16 | const raw = input.trim() 17 | const urls = raw 18 | .split(/\s+/) 19 | .map((url) => url.trim()) 20 | .map((url) => (url.startsWith('http://') || url.startsWith('https://') ? url : `https://${url}`)) 21 | modal.resolve(urls) 22 | modal.hide() 23 | } 24 | const onInput = (e: React.ChangeEvent) => { 25 | setInput(e.target.value) 26 | } 27 | const onKeyDown = (event: React.KeyboardEvent) => { 28 | const ctrlOrCmd = event.ctrlKey || event.metaKey 29 | // ctrl + enter 提交 30 | if (event.keyCode === 13 && ctrlOrCmd) { 31 | event.preventDefault() 32 | onSubmit() 33 | return 34 | } 35 | } 36 | 37 | return ( 38 | { 41 | modal.resolve() 42 | modal.hide() 43 | }} 44 | fullWidth 45 | > 46 | {t('Attach Link')} 47 | 48 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ) 66 | }) 67 | 68 | export default AttachLink 69 | -------------------------------------------------------------------------------- /src/renderer/modals/ClearSessionList.tsx: -------------------------------------------------------------------------------- 1 | import NiceModal, { muiDialogV5, useModal } from '@ebay/nice-modal-react' 2 | import { ChangeEvent, useEffect, useState } from 'react' 3 | import { Input, Button, Dialog, DialogContent, DialogActions, DialogTitle, DialogContentText } from '@mui/material' 4 | import { useTranslation, Trans } from 'react-i18next' 5 | import * as sessionActions from '../stores/sessionActions' 6 | import { trackingEvent } from '@/packages/event' 7 | 8 | const ClearSessionList = NiceModal.create(() => { 9 | const modal = useModal() 10 | const { t } = useTranslation() 11 | const [value, setValue] = useState(100) 12 | const handleInput = (event: ChangeEvent) => { 13 | const int = parseInt(event.target.value || '0') 14 | if (int >= 0) { 15 | setValue(int) 16 | } 17 | } 18 | 19 | useEffect(() => { 20 | trackingEvent('clear_conversation_list_window', { event_category: 'screen_view' }) 21 | }, []) 22 | 23 | const clean = () => { 24 | sessionActions.clearConversationList(value) 25 | trackingEvent('clear_conversation_list', { event_category: 'user' }) 26 | handleClose() 27 | } 28 | 29 | const handleClose = () => { 30 | modal.resolve() 31 | modal.hide() 32 | } 33 | 34 | return ( 35 | { 38 | modal.resolve() 39 | modal.hide() 40 | }} 41 | > 42 | {t('Clear Conversation List')} 43 | 44 | 45 | , 55 | ]} 56 | /> 57 | 58 | 59 | 60 | 61 | 64 | 65 | 66 | ) 67 | }) 68 | 69 | export default ClearSessionList 70 | -------------------------------------------------------------------------------- /src/renderer/modals/index.tsx: -------------------------------------------------------------------------------- 1 | import NiceModal from '@ebay/nice-modal-react' 2 | import Welcome from './Welcome' 3 | import ProviderSelector from './ProviderSelector' 4 | import SessionSettings from './SessionSettings' 5 | import AppStoreRating from './AppStoreRating' 6 | import ArtifactPreview from './ArtifactPreview' 7 | import ClearSessionList from './ClearSessionList' 8 | import ExportChat from './ExportChat' 9 | import MessageEdit from './MessageEdit' 10 | import AttachLink from './AttachLink' 11 | import ReportContent from './ReportContent' 12 | import ModelEdit from './ModelEdit' 13 | 14 | NiceModal.register('welcome', Welcome) 15 | NiceModal.register('provider-selector', ProviderSelector) 16 | NiceModal.register('session-settings', SessionSettings) 17 | NiceModal.register('app-store-rating', AppStoreRating) 18 | NiceModal.register('artifact-preview', ArtifactPreview) 19 | NiceModal.register('clear-session-list', ClearSessionList) 20 | NiceModal.register('export-chat', ExportChat) 21 | NiceModal.register('message-edit', MessageEdit) 22 | NiceModal.register('attach-link', AttachLink) 23 | NiceModal.register('report-content', ReportContent) 24 | NiceModal.register('model-edit', ModelEdit) 25 | -------------------------------------------------------------------------------- /src/renderer/packages/apple_app_store.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react' 2 | import { store as keypairStore } from './keypairs' 3 | import { CHATBOX_BUILD_PLATFORM } from '../variables' 4 | import NiceModal from '@ebay/nice-modal-react' 5 | 6 | // 本次启动是否已经引导过用户评价 App Store 7 | let hasOpenAppStoreReviewPage = false 8 | 9 | export async function tryOpenAppStoreReviewPage() { 10 | try { 11 | if (hasOpenAppStoreReviewPage) { 12 | return 13 | } 14 | if (await keypairStore.getItem('appStoreRatingClicked')) { 15 | return 16 | } 17 | const lastAppStoreReviewTime = (await keypairStore.getItem('lastAppStoreReviewTime')) || 0 18 | const now = Date.now() 19 | if (now - lastAppStoreReviewTime < 1000 * 60 * 60 * 24 * 30) { 20 | // 30 天 21 | return 22 | } 23 | hasOpenAppStoreReviewPage = true 24 | await keypairStore.setItem('lastAppStoreReviewTime', now) 25 | NiceModal.show('app-store-rating') 26 | } catch (e) { 27 | console.error(e) 28 | Sentry.captureException(e) 29 | } 30 | } 31 | 32 | // 记录App Store评分弹窗点击 33 | export async function recordAppStoreRatingClick() { 34 | await keypairStore.setItem('appStoreRatingClicked', true) 35 | } 36 | 37 | let tickCount = 0 38 | export function tickAfterMessageGenerated() { 39 | if (CHATBOX_BUILD_PLATFORM !== 'ios') { 40 | return 41 | } 42 | tickCount++ 43 | if (tickCount % 4 === 0) { 44 | tryOpenAppStoreReviewPage() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/packages/base64.test.ts: -------------------------------------------------------------------------------- 1 | import { parseImage } from './base64' 2 | 3 | describe('parseImage', () => { 4 | it('should parse base64 image data correctly', () => { 5 | const base64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAChA...' 6 | const result = parseImage(base64) 7 | expect(result).toEqual({ type: 'image/png', data: 'iVBORw0KGgoAAAANSUhEUgAAChA...' }) 8 | }) 9 | 10 | it('should handle base64 data without a type correctly', () => { 11 | const base64 = 'iVBORw0KGgoAAAANSUhEUgAAChA...' 12 | const result = parseImage(base64) 13 | expect(result).toEqual({ type: '', data: '' }) 14 | }) 15 | 16 | it('should handle empty base64 data correctly', () => { 17 | const base64 = '' 18 | const result = parseImage(base64) 19 | expect(result).toEqual({ type: '', data: '' }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/renderer/packages/base64.ts: -------------------------------------------------------------------------------- 1 | export function parseImage(base64: string) { 2 | // data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAChA... 3 | base64 = base64.replace(/^data:/, '') 4 | const markIndex = base64.indexOf(';') 5 | if (markIndex < 0) { 6 | return { type: '', data: '' } 7 | } 8 | const type = base64.slice(0, markIndex) 9 | base64 = base64.slice(markIndex + 1) 10 | base64 = base64.replace(/^base64,/, '') 11 | const data = base64 12 | return { type, data } 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/packages/cache.ts: -------------------------------------------------------------------------------- 1 | import localforage from 'localforage' 2 | 3 | export const store = localforage.createInstance({ name: 'chatboxcache' }) 4 | 5 | export interface CacheItem { 6 | value: T 7 | expireAt: number 8 | } 9 | 10 | export async function cache( 11 | key: string, 12 | getter: () => Promise, 13 | options: { 14 | ttl: number // 缓存过期时间,单位为毫秒 15 | refreshFallbackToCache?: boolean // 如果刷新时获取新值失败,是否从缓存中继续使用过期的旧值 16 | } 17 | ): Promise { 18 | const cachedStr = await store.getItem(key) 19 | let cache: CacheItem | null = null 20 | 21 | if (cachedStr) { 22 | try { 23 | cache = JSON.parse(cachedStr) 24 | } catch (e) { 25 | console.error(`Error parsing cache for key ${key}:`, e) 26 | } 27 | } 28 | 29 | if (cache && cache.expireAt > Date.now()) { 30 | return cache.value 31 | } 32 | 33 | try { 34 | const newValue = await getter() 35 | cache = { 36 | value: newValue, 37 | expireAt: Date.now() + options.ttl, 38 | } 39 | await store.setItem(key, JSON.stringify(cache)) 40 | return newValue 41 | } catch (e) { 42 | if (options.refreshFallbackToCache && cache) { 43 | return cache.value 44 | } 45 | throw e 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/packages/codeblock_state_recorder.ts: -------------------------------------------------------------------------------- 1 | export interface CodeblockState { 2 | collapsed: boolean 3 | shouldCollapse: boolean 4 | lines: number 5 | } 6 | 7 | const memoryStore = new Map() 8 | 9 | function getID(content: string, language: string) { 10 | let hash = 0 11 | const combined = content + language 12 | for (let i = 0; i < combined.length; i++) { 13 | const char = combined.charCodeAt(i) 14 | hash = (hash << 5) - hash + char 15 | hash |= 0 // Convert to 32bit integer 16 | } 17 | return hash.toString() 18 | } 19 | 20 | interface Options { 21 | content: string 22 | language: string 23 | generating?: boolean 24 | preferCollapsed?: boolean 25 | } 26 | 27 | export function needCollapse(options: Options): CodeblockState { 28 | if (options.generating) { 29 | return { 30 | collapsed: false, 31 | shouldCollapse: false, 32 | lines: 0, 33 | } 34 | } 35 | const id = getID(options.content, options.language) 36 | if (memoryStore.has(id)) { 37 | return memoryStore.get(id)! 38 | } 39 | return calculateState(options) 40 | } 41 | 42 | export function saveState(options: Options & { collapsed: boolean }) { 43 | const id = getID(options.content, options.language) 44 | const newState = calculateState(options) 45 | newState.collapsed = options.collapsed 46 | memoryStore.set(id, newState) 47 | return newState 48 | } 49 | 50 | export function calculateState(options: Options): CodeblockState { 51 | const lines = options.content.split('\n').length 52 | const shouldCollapse = !!options.preferCollapsed && lines > 6 53 | const collapsed = shouldCollapse 54 | return { 55 | collapsed, 56 | shouldCollapse, 57 | lines, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/renderer/packages/event.ts: -------------------------------------------------------------------------------- 1 | import platform from '@/platform' 2 | import { allowReportingAndTrackingAtom } from '@/stores/atoms' 3 | import { getDefaultStore } from 'jotai' 4 | 5 | export function trackingEvent(name: string, params: { [key: string]: string } = {}) { 6 | const store = getDefaultStore() 7 | const allowReportingAndTracking = store.get(allowReportingAndTrackingAtom) 8 | if (!allowReportingAndTracking) { 9 | return 10 | } 11 | platform.trackingEvent(name, params) 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/packages/filetype.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 可以判断当前文件是否为常见的文本文件 3 | */ 4 | export function isTextFile(file: File) { 5 | return ( 6 | file.type.startsWith('text/') || 7 | file.type === 'application/json' || 8 | file.type === 'application/xml' || 9 | file.type === 'application/x-yaml' || 10 | file.type === 'application/x-toml' || 11 | file.type === 'application/x-sh' || 12 | file.type === 'application/javascript' || 13 | file.type === '' 14 | ) 15 | } 16 | 17 | export function isPdf(file: File) { 18 | return file.type === 'application/pdf' 19 | } 20 | 21 | export function isWord(file: File) { 22 | return ( 23 | file.type === 'application/msword' || 24 | file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' 25 | ) 26 | } 27 | 28 | export function isPPT(file: File) { 29 | return ( 30 | file.type === 'application/vnd.ms-powerpoint' || 31 | file.type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' 32 | ) 33 | } 34 | 35 | export function isExcel(file: File) { 36 | return ( 37 | file.type === 'application/vnd.ms-excel' || 38 | file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/packages/keypairs.ts: -------------------------------------------------------------------------------- 1 | import localforage from 'localforage' 2 | 3 | export const store = localforage.createInstance({ name: 'chatboxkeypair' }) 4 | -------------------------------------------------------------------------------- /src/renderer/packages/local-parser.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | import platform from '@/platform' 3 | import * as remote from './remote' 4 | 5 | export async function parseTextFile(file: File, options: { maxLength?: number } = {}) { 6 | let text = await file.text() 7 | if (options.maxLength) { 8 | text = text.trim().slice(0, options.maxLength) 9 | } 10 | const key = `parseFile-` + uuidv4() 11 | await platform.setStoreBlob(key, text) 12 | return { key } 13 | } 14 | 15 | export async function parseUrl(url: string) { 16 | const result = await remote.parseUserLinkFree({ url }) 17 | const key = `parseUrl-` + uuidv4() 18 | await platform.setStoreBlob(key, result.text) 19 | return { key, title: result.title } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/packages/model-calls/index.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../../../shared/types' 2 | import { ModelInterface } from '../models/types' 3 | 4 | export { streamText } from './stream-text' 5 | 6 | export async function generateText(model: ModelInterface, messages: Message[]) { 7 | return model.chat(messages, {}) 8 | } 9 | 10 | export async function generateImage( 11 | model: ModelInterface, 12 | params: { prompt: string; num: number; signal?: AbortSignal; callback?: (picBase64: string) => void } 13 | ) { 14 | return model.paint(params.prompt, params.num, params.callback, params.signal) 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/azure-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, SessionType } from 'src/shared/types' 2 | import { ModelSettingUtil } from './interface' 3 | import AzureOpenAI from '../models/azure' 4 | import BaseConfig from './base-config' 5 | 6 | export default class AzureSettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.Azure 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | if (sessionType === 'picture') { 14 | return `Azure OpenAI API (${model})` 15 | } else { 16 | return `Azure OpenAI API (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 17 | } 18 | } 19 | 20 | public getLocalOptionGroups() { 21 | // FIXME: 22 | return [] 23 | } 24 | 25 | protected async listProviderModels() { 26 | return [] 27 | } 28 | 29 | public isCurrentModelSupportImageInput(model: string) { 30 | return AzureOpenAI.helpers.isModelSupportVision(model) 31 | } 32 | 33 | public isCurrentModelSupportToolUse(model: string) { 34 | return AzureOpenAI.helpers.isModelSupportToolUse(model) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/chatglm-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, Session, SessionType, Settings } from 'src/shared/types' 2 | import ChatGLM, { chatglmModels } from '../models/chatglm' 3 | import BaseConfig from './base-config' 4 | import { ModelSettingUtil } from './interface' 5 | 6 | export default class ChatGLMSettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.ChatGLM6B 8 | async getCurrentModelDisplayName(model: string): Promise { 9 | return model 10 | } 11 | 12 | public getLocalOptionGroups() { 13 | return [{ options: chatglmModels.map((model) => ({ label: model, value: model })) }] 14 | } 15 | 16 | protected async listProviderModels(settings: ProviderSettings) { 17 | return [] 18 | } 19 | 20 | public isCurrentModelSupportImageInput(model: string) { 21 | return ChatGLM.helpers.isModelSupportVision(model) 22 | } 23 | 24 | public isCurrentModelSupportToolUse(model: string) { 25 | return ChatGLM.helpers.isModelSupportToolUse(model) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/claude-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, Session, SessionType, Settings } from 'src/shared/types' 2 | import Claude, { claudeModels } from '../models/claude' 3 | import BaseConfig from './base-config' 4 | import { ModelSettingUtil } from './interface' 5 | 6 | export default class ClaudeSettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.Claude 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | return `Claude API (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 14 | } 15 | 16 | public getLocalOptionGroups() { 17 | return [ 18 | { 19 | options: claudeModels.map((value) => ({ 20 | label: value, 21 | value: value, 22 | })), 23 | }, 24 | ] 25 | } 26 | 27 | protected async listProviderModels(settings: ProviderSettings) { 28 | const claude = new Claude({ 29 | claudeApiHost: settings.apiHost!, 30 | claudeApiKey: settings.apiKey!, 31 | claudeModel: '', 32 | }) 33 | return claude.listModels() 34 | } 35 | 36 | public isCurrentModelSupportImageInput(model: string) { 37 | return Claude.helpers.isModelSupportVision(model) 38 | } 39 | 40 | public isCurrentModelSupportToolUse(model: string) { 41 | return Claude.helpers.isModelSupportToolUse(model) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/custom-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, SessionType } from 'src/shared/types' 2 | import { ModelSettingUtil } from './interface' 3 | import BaseConfig from './base-config' 4 | 5 | // TODO: 重新实现 6 | export default class CustomModelSettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.Custom 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | return `Custom API (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 14 | } 15 | 16 | public getLocalOptionGroups() { 17 | return [] 18 | } 19 | 20 | protected async listProviderModels(settings: ProviderSettings) { 21 | return [] 22 | } 23 | 24 | isCurrentModelSupportImageInput(model: string): boolean { 25 | return true 26 | } 27 | 28 | isCurrentModelSupportToolUse(model: string): boolean { 29 | return false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/deepseek-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, Session, SessionType, Settings } from 'src/shared/types' 2 | import { ModelSettingUtil } from './interface' 3 | import BaseConfig from './base-config' 4 | import DeepSeek, { deepSeekModels } from '../models/deepseek' 5 | 6 | export default class DeepSeekSettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.DeepSeek 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | return `DeepSeek API (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 14 | } 15 | 16 | public getLocalOptionGroups() { 17 | return [ 18 | { 19 | options: deepSeekModels.map((value) => { 20 | return { 21 | label: value, 22 | value: value, 23 | } 24 | }), 25 | }, 26 | ] 27 | } 28 | 29 | protected async listProviderModels(settings: ProviderSettings) { 30 | const deepSeek = new DeepSeek({ 31 | deepseekAPIKey: settings.apiKey!, 32 | deepseekModel: '', 33 | }) 34 | return deepSeek.listModels() 35 | } 36 | 37 | public isCurrentModelSupportImageInput(model: string) { 38 | return DeepSeek.helpers.isModelSupportVision(model) 39 | } 40 | 41 | public isCurrentModelSupportToolUse(model: string) { 42 | return DeepSeek.helpers.isModelSupportToolUse(model) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/gemini-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, Session, SessionType, Settings } from 'src/shared/types' 2 | import { ModelSettingUtil } from './interface' 3 | import Gemini, { GeminiModel, geminiModels } from '../models/gemini' 4 | import BaseConfig from './base-config' 5 | 6 | export default class GeminiSettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.Gemini 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | return `Gemini API (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 14 | } 15 | 16 | public getLocalOptionGroups() { 17 | return [ 18 | { 19 | options: geminiModels.map((value) => { 20 | return { 21 | label: value, 22 | value: value, 23 | } 24 | }), 25 | }, 26 | ] 27 | } 28 | 29 | protected async listProviderModels(settings: ProviderSettings) { 30 | const gemini = new Gemini({ 31 | geminiAPIHost: settings.apiHost!, 32 | geminiAPIKey: settings.apiKey!, 33 | geminiModel: 'gemini-pro', 34 | temperature: 0, 35 | }) 36 | return gemini.listModels() 37 | } 38 | 39 | public isCurrentModelSupportImageInput(model: string) { 40 | return Gemini.helpers.isModelSupportVision(model) 41 | } 42 | 43 | public isCurrentModelSupportToolUse(model: string) { 44 | return Gemini.helpers.isModelSupportToolUse(model) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/groq-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, Session, SessionType, Settings } from 'src/shared/types' 2 | import Groq from '../models/groq' 3 | import BaseConfig from './base-config' 4 | import { ModelSettingUtil } from './interface' 5 | 6 | export default class GroqSettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.Groq 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | return `Groq API (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 14 | } 15 | 16 | public getLocalOptionGroups() { 17 | return [] 18 | } 19 | 20 | protected async listProviderModels(settings: ProviderSettings) { 21 | const groq = new Groq({ 22 | groqAPIKey: settings.apiKey!, 23 | groqModel: '', 24 | temperature: 0, 25 | }) 26 | return groq.listModels() 27 | } 28 | 29 | public isCurrentModelSupportImageInput(model: string) { 30 | return Groq.helpers.isModelSupportVision(model) 31 | } 32 | 33 | public isCurrentModelSupportToolUse(model: string) { 34 | return Groq.helpers.isModelSupportToolUse(model) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/interface.ts: -------------------------------------------------------------------------------- 1 | import { ModelOptionGroup, ModelProvider, ProviderSettings, SessionType } from 'src/shared/types' 2 | 3 | export interface ModelSettingUtil { 4 | provider: ModelProvider 5 | // 用在消息下面展示的模型名称 6 | getCurrentModelDisplayName( 7 | model: string, 8 | sessionType: SessionType, 9 | providerSettings?: ProviderSettings 10 | ): Promise 11 | // 获取该provider在代码里写死的模型组 12 | getLocalOptionGroups(): ModelOptionGroup[] 13 | // 获取该provider远程的模型组 14 | getMergeOptionGroups(providerSettings: ProviderSettings): Promise 15 | // 判断模型对feature的支持 16 | isCurrentModelSupportImageInput(model: string): boolean 17 | isCurrentModelSupportToolUse(model: string): boolean 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/lmstudio-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, Session, SessionType, Settings } from 'src/shared/types' 2 | import { ModelSettingUtil } from './interface' 3 | import BaseConfig from './base-config' 4 | import LMStudio from '../models/lmstudio' 5 | 6 | export default class LMStudioSettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.LMStudio 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | return `LM Studio (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 14 | } 15 | 16 | public getLocalOptionGroups() { 17 | return [] 18 | } 19 | 20 | protected async listProviderModels(settings: ProviderSettings) { 21 | const lmStudio = new LMStudio({ 22 | lmStudioHost: settings.apiHost!, 23 | lmStudioModel: '', 24 | }) 25 | return lmStudio.listModels() 26 | } 27 | 28 | isCurrentModelSupportImageInput(model: string): boolean { 29 | return LMStudio.helpers.isModelSupportVision(model) 30 | } 31 | 32 | isCurrentModelSupportToolUse(model: string): boolean { 33 | return LMStudio.helpers.isModelSupportToolUse(model) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/ollama-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, Session, SessionType, Settings } from 'src/shared/types' 2 | import { ModelSettingUtil } from './interface' 3 | import Ollama from '../models/ollama' 4 | import BaseConfig from './base-config' 5 | 6 | export default class OllamaSettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.Ollama 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | return `Ollama (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 14 | } 15 | 16 | public getLocalOptionGroups() { 17 | return [] 18 | } 19 | 20 | protected async listProviderModels(settings: ProviderSettings) { 21 | const ollama = new Ollama({ ollamaHost: settings.apiHost!, ollamaModel: '', temperature: 0 }) 22 | return ollama.listModels() 23 | } 24 | 25 | isCurrentModelSupportImageInput(model: string): boolean { 26 | return Ollama.helpers.isModelSupportVision(model) 27 | } 28 | 29 | isCurrentModelSupportToolUse(model: string): boolean { 30 | return Ollama.helpers.isModelSupportToolUse(model) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/openai-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, SessionType } from 'src/shared/types' 2 | import { ModelSettingUtil } from './interface' 3 | import OpenAI, { openaiModelConfigs } from '../models/openai' 4 | import { uniq } from 'lodash' 5 | import BaseConfig from './base-config' 6 | 7 | export default class OpenAISettingUtil extends BaseConfig implements ModelSettingUtil { 8 | public provider: ModelProvider = ModelProvider.OpenAI 9 | async getCurrentModelDisplayName( 10 | model: string, 11 | sessionType: SessionType, 12 | providerSettings?: ProviderSettings 13 | ): Promise { 14 | if (sessionType === 'picture') { 15 | return `OpenAI API (DALL-E-3)` 16 | } else { 17 | return `OpenAI API (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 18 | } 19 | } 20 | 21 | public getLocalOptionGroups() { 22 | let models = Array.from(Object.keys(openaiModelConfigs)).sort() 23 | models = uniq(models) 24 | return [ 25 | { 26 | options: models.map((value) => ({ 27 | label: value, 28 | value: value, 29 | })), 30 | }, 31 | ] 32 | } 33 | 34 | protected async listProviderModels() { 35 | return [] 36 | } 37 | 38 | isCurrentModelSupportImageInput(model: string): boolean { 39 | return OpenAI.helpers.isModelSupportVision(model) 40 | } 41 | 42 | isCurrentModelSupportToolUse(model: string): boolean { 43 | return OpenAI.helpers.isModelSupportToolUse(model) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/perplexity-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, Session, SessionType, Settings } from 'src/shared/types' 2 | import Perplexity, { perplexityModels } from '../models/perplexity' 3 | import BaseConfig from './base-config' 4 | import { ModelSettingUtil } from './interface' 5 | 6 | export default class PerplexitySettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.Perplexity 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | return `Perplexity API (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 14 | } 15 | 16 | public getLocalOptionGroups() { 17 | return [{ options: perplexityModels.map((model) => ({ label: model, value: model })) }] 18 | } 19 | 20 | protected async listProviderModels(settings: ProviderSettings) { 21 | return [] 22 | } 23 | 24 | isCurrentModelSupportImageInput(model: string): boolean { 25 | return Perplexity.helpers.isModelSupportVision(model) 26 | } 27 | 28 | isCurrentModelSupportToolUse(model: string): boolean { 29 | return Perplexity.helpers.isModelSupportToolUse(model) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/siliconflow-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, SessionType } from 'src/shared/types' 2 | import { ModelSettingUtil } from './interface' 3 | import BaseConfig from './base-config' 4 | import SiliconFlow, { siliconFlowModels } from '../models/siliconflow' 5 | 6 | export default class SiliconFlowSettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.SiliconFlow 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | return `SiliconFlow API (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 14 | } 15 | 16 | public getLocalOptionGroups() { 17 | return [ 18 | { 19 | options: siliconFlowModels.map((value) => { 20 | return { 21 | label: value, 22 | value: value, 23 | } 24 | }), 25 | }, 26 | ] 27 | } 28 | 29 | protected async listProviderModels(settings: ProviderSettings) { 30 | const siliconFlow = new SiliconFlow({ 31 | siliconCloudKey: settings.apiKey!, 32 | siliconCloudModel: '', 33 | }) 34 | return siliconFlow.listModels() 35 | } 36 | 37 | isCurrentModelSupportImageInput(model: string): boolean { 38 | return SiliconFlow.helpers.isModelSupportVision(model) 39 | } 40 | 41 | isCurrentModelSupportToolUse(model: string): boolean { 42 | return SiliconFlow.helpers.isModelSupportToolUse(model) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/packages/model-setting-utils/xai-setting-util.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider, ProviderSettings, SessionType } from 'src/shared/types' 2 | import { ModelSettingUtil } from './interface' 3 | import BaseConfig from './base-config' 4 | import XAI, { xAIModels } from '../models/xai' 5 | 6 | export default class XAISettingUtil extends BaseConfig implements ModelSettingUtil { 7 | public provider: ModelProvider = ModelProvider.XAI 8 | async getCurrentModelDisplayName( 9 | model: string, 10 | sessionType: SessionType, 11 | providerSettings?: ProviderSettings 12 | ): Promise { 13 | return `xAI API (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})` 14 | } 15 | 16 | public getLocalOptionGroups() { 17 | return [ 18 | { 19 | options: xAIModels.map((value) => { 20 | return { 21 | label: value, 22 | value: value, 23 | } 24 | }), 25 | }, 26 | ] 27 | } 28 | 29 | protected async listProviderModels(settings: ProviderSettings) { 30 | const xai = new XAI({ xAIKey: settings.apiKey!, xAIModel: '' }) 31 | return xai.listModels() 32 | } 33 | 34 | isCurrentModelSupportImageInput(model: string): boolean { 35 | return XAI.helpers.isModelSupportVision(model) 36 | } 37 | 38 | isCurrentModelSupportToolUse(model: string): boolean { 39 | return XAI.helpers.isModelSupportToolUse(model) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/packages/models/azure.ts: -------------------------------------------------------------------------------- 1 | import { createAzure } from '@ai-sdk/azure' 2 | import AbstractAISDKModel from './abstract-ai-sdk' 3 | import { ModelHelpers } from './types' 4 | import { normalizeAzureEndpoint } from './llm_utils' 5 | 6 | const helpers: ModelHelpers = { 7 | isModelSupportVision: (model: string) => { 8 | return true 9 | }, 10 | isModelSupportToolUse: (model: string) => { 11 | return true 12 | }, 13 | } 14 | 15 | interface Options { 16 | azureEndpoint: string 17 | azureDeploymentName: string 18 | azureDalleDeploymentName: string // dall-e-3 的部署名称 19 | azureApikey: string 20 | azureApiVersion: string 21 | 22 | // openaiMaxTokens: number 23 | temperature: number 24 | topP: number 25 | 26 | dalleStyle: 'vivid' | 'natural' 27 | imageGenerateNum: number // 生成图片的数量 28 | 29 | injectDefaultMetadata: boolean 30 | } 31 | 32 | export default class AzureOpenAI extends AbstractAISDKModel { 33 | public name = 'Azure OpenAI' 34 | public static helpers = helpers 35 | 36 | constructor(public options: Options) { 37 | super() 38 | } 39 | 40 | private getProvider() { 41 | return createAzure({ 42 | apiKey: this.options.azureApikey, 43 | apiVersion: this.options.azureApiVersion, 44 | baseURL: normalizeAzureEndpoint(this.options.azureEndpoint).endpoint, 45 | }) 46 | } 47 | 48 | protected getChatModel() { 49 | const provider = this.getProvider() 50 | return provider.chat(this.options.azureDeploymentName) 51 | } 52 | 53 | protected getImageModel() { 54 | const provider = this.getProvider() 55 | return provider.imageModel(this.options.azureDalleDeploymentName) 56 | } 57 | 58 | public isSupportToolUse() { 59 | return helpers.isModelSupportToolUse(this.options.azureDeploymentName) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/packages/models/chatglm.ts: -------------------------------------------------------------------------------- 1 | import { ModelMeta } from 'src/shared/types' 2 | import { ModelHelpers } from './types' 3 | import OpenAICompatible from './openai-compatible' 4 | 5 | const modelConfig: ModelMeta = { 6 | 'glm-4-plus': { 7 | contextWindow: 128_000, 8 | functionCalling: true, 9 | vision: false, 10 | }, 11 | 'glm-4-air': { 12 | contextWindow: 128_000, 13 | functionCalling: true, 14 | vision: false, 15 | }, 16 | 'glm-4-flash': { 17 | contextWindow: 128_000, 18 | functionCalling: true, 19 | }, 20 | 'glm-4v-plus-0111': { 21 | contextWindow: 16_000, 22 | vision: true, 23 | }, 24 | 'glm-4v-flash': { 25 | contextWindow: 16_000, 26 | vision: true, 27 | }, 28 | } 29 | 30 | export const chatglmModels = Object.keys(modelConfig) 31 | 32 | const helpers: ModelHelpers = { 33 | isModelSupportVision: (model: string) => { 34 | return modelConfig[model]?.vision ?? false 35 | }, 36 | isModelSupportToolUse: (model: string) => { 37 | return modelConfig[model]?.functionCalling ?? false 38 | }, 39 | } 40 | 41 | interface Options { 42 | chatglmApiKey: string 43 | chatglmModel: string 44 | } 45 | 46 | export default class ChatGLM extends OpenAICompatible { 47 | public name = 'ChatGLM' 48 | public static helpers = helpers 49 | 50 | constructor(public options: Options) { 51 | super({ 52 | apiKey: options.chatglmApiKey, 53 | apiHost: 'https://open.bigmodel.cn/api/paas/v4/', 54 | model: options.chatglmModel, 55 | }) 56 | } 57 | 58 | isSupportToolUse() { 59 | return helpers.isModelSupportToolUse(this.options.chatglmModel) 60 | } 61 | 62 | public async listModels() { 63 | return [] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/renderer/packages/models/deepseek.ts: -------------------------------------------------------------------------------- 1 | import { ModelHelpers } from './types' 2 | import OpenAICompatible from './openai-compatible' 3 | 4 | // https://api-docs.deepseek.com/zh-cn/quick_start/pricing 5 | export const modelConfig = { 6 | 'deepseek-chat': { 7 | contextWindow: 64_000, 8 | maxTokens: 8_000, 9 | vision: false, 10 | }, 11 | 'deepseek-coder': { 12 | contextWindow: 64_000, 13 | maxTokens: 8_000, 14 | vision: false, 15 | }, 16 | 'deepseek-reasoner': { 17 | contextWindow: 64_000, 18 | maxTokens: 8_000, 19 | vision: false, 20 | }, 21 | } 22 | 23 | export const deepSeekModels = Object.keys(modelConfig) 24 | 25 | const helpers: ModelHelpers = { 26 | isModelSupportVision: (model: string) => { 27 | return false 28 | }, 29 | isModelSupportToolUse: (model: string) => { 30 | return false 31 | }, 32 | } 33 | 34 | interface Options { 35 | deepseekAPIKey: string 36 | deepseekModel: string 37 | temperature?: number 38 | topP?: number 39 | } 40 | 41 | export default class DeepSeek extends OpenAICompatible { 42 | public name = 'DeepSeek' 43 | public static helpers = helpers 44 | 45 | constructor(public options: Options) { 46 | super({ 47 | apiKey: options.deepseekAPIKey, 48 | apiHost: 'https://api.deepseek.com/v1', 49 | model: options.deepseekModel, 50 | temperature: options.deepseekModel === 'deepseek-reasoner' ? undefined : options.temperature, 51 | topP: options.deepseekModel === 'deepseek-reasoner' ? undefined : options.topP, 52 | }) 53 | } 54 | 55 | isSupportToolUse() { 56 | return helpers.isModelSupportToolUse(this.options.deepseekModel) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/renderer/packages/models/groq.ts: -------------------------------------------------------------------------------- 1 | import { ModelHelpers } from './types' 2 | import OpenAICompatible from './openai-compatible' 3 | 4 | const helpers: ModelHelpers = { 5 | isModelSupportVision: (model: string) => { 6 | return model.includes('vision') 7 | }, 8 | isModelSupportToolUse: (model: string) => { 9 | return false 10 | }, 11 | } 12 | 13 | interface Options { 14 | groqAPIKey: string 15 | groqModel: string 16 | temperature: number 17 | } 18 | 19 | export default class Groq extends OpenAICompatible { 20 | public name = 'Groq' 21 | public static helpers = helpers 22 | 23 | constructor(public options: Options) { 24 | super({ 25 | apiKey: options.groqAPIKey, 26 | apiHost: 'https://api.groq.com/openai/v1', 27 | model: options.groqModel, 28 | temperature: options.temperature, 29 | }) 30 | } 31 | 32 | isSupportToolUse() { 33 | return helpers.isModelSupportToolUse(this.options.groqModel) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/packages/models/lmstudio.ts: -------------------------------------------------------------------------------- 1 | import { ModelHelpers } from './types' 2 | import OpenAICompatible from './openai-compatible' 3 | import { normalizeOpenAIApiHostAndPath } from './llm_utils' 4 | 5 | const helpers: ModelHelpers = { 6 | isModelSupportVision: (model: string) => { 7 | model = model.toLowerCase() 8 | return model.includes('vision') || model.includes('llava') 9 | }, 10 | isModelSupportToolUse: (model: string) => { 11 | return false 12 | }, 13 | } 14 | 15 | interface Options { 16 | lmStudioHost: string 17 | lmStudioModel: string 18 | temperature?: number 19 | topP?: number 20 | } 21 | 22 | export default class LMStudio extends OpenAICompatible { 23 | public name = 'LM Studio' 24 | public static helpers = helpers 25 | 26 | constructor(public options: Options) { 27 | super({ 28 | apiKey: '', 29 | apiHost: normalizeOpenAIApiHostAndPath({ apiHost: options.lmStudioHost }).apiHost, 30 | model: options.lmStudioModel, 31 | temperature: options.temperature, 32 | topP: options.topP, 33 | }) 34 | } 35 | 36 | isSupportToolUse() { 37 | return helpers.isModelSupportToolUse(this.options.lmStudioModel) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/packages/models/ollama.ts: -------------------------------------------------------------------------------- 1 | import type { ModelHelpers } from './types' 2 | import OpenAICompatible from './openai-compatible' 3 | import { normalizeOpenAIApiHostAndPath } from './llm_utils' 4 | 5 | const helpers: ModelHelpers = { 6 | isModelSupportVision: (model: string) => { 7 | return [ 8 | 'gemma3', 9 | 'llava', 10 | 'llama3.2-vision', 11 | 'llava-llama3', 12 | 'moondream', 13 | 'bakllava', 14 | 'llava-phi3', 15 | 'granite3.2-vision', 16 | ].some((m) => model.startsWith(m)) 17 | }, 18 | isModelSupportToolUse: (model: string) => { 19 | return [ 20 | 'qwq', 21 | 'llama3.3', 22 | 'llama3.2', 23 | 'llama3.1', 24 | 'mistral', 25 | 'qwen2.5', 26 | 'qwen2.5-coder', 27 | 'qwen2', 28 | 'mistral-nemo', 29 | 'mixtral', 30 | 'smollm2', 31 | 'mistral-small', 32 | 'command-r', 33 | 'hermes3', 34 | 'mistral-large', 35 | ].some((m) => model.startsWith(m)) 36 | }, 37 | } 38 | 39 | interface Options { 40 | ollamaHost: string 41 | ollamaModel: string 42 | temperature: number 43 | } 44 | 45 | export default class Ollama extends OpenAICompatible { 46 | public name = 'Ollama' 47 | public static helpers = helpers 48 | 49 | constructor(public options: Options) { 50 | super({ 51 | apiKey: 'ollama', 52 | apiHost: normalizeOpenAIApiHostAndPath({ apiHost: options.ollamaHost }).apiHost, 53 | model: options.ollamaModel, 54 | temperature: options.temperature, 55 | }) 56 | } 57 | 58 | isSupportToolUse(): boolean { 59 | return helpers.isModelSupportToolUse(this.options.ollamaModel) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/packages/models/openai-compatible.ts: -------------------------------------------------------------------------------- 1 | import { apiRequest, fetchWithProxy } from '@/utils/request' 2 | import { createOpenAICompatible } from '@ai-sdk/openai-compatible' 3 | import AbstractAISDKModel from './abstract-ai-sdk' 4 | import { ModelInterface } from './types' 5 | import { ApiError } from './errors' 6 | 7 | interface OpenAICompatibleSettings { 8 | apiKey: string 9 | apiHost: string 10 | model: string 11 | temperature?: number 12 | topP?: number 13 | useProxy?: boolean 14 | } 15 | 16 | export default abstract class OpenAICompatible extends AbstractAISDKModel implements ModelInterface { 17 | public name = 'OpenAI Compatible' 18 | 19 | constructor(private settings: OpenAICompatibleSettings) { 20 | super() 21 | } 22 | 23 | protected getCallSettings() { 24 | return { 25 | temperature: this.settings.temperature, 26 | topP: this.settings.topP, 27 | } 28 | } 29 | 30 | protected getChatModel() { 31 | const provider = createOpenAICompatible({ 32 | name: this.name, 33 | apiKey: this.settings.apiKey, 34 | baseURL: this.settings.apiHost, 35 | fetch: this.settings.useProxy ? fetchWithProxy : undefined, 36 | }) 37 | return provider.languageModel(this.settings.model) 38 | } 39 | 40 | public abstract isSupportToolUse(): boolean 41 | 42 | public async listModels(): Promise { 43 | return fetchRemoteModels({ 44 | apiHost: this.settings.apiHost, 45 | apiKey: this.settings.apiKey, 46 | useProxy: this.settings.useProxy, 47 | }).catch((err) => { 48 | console.error(err) 49 | return [] 50 | }) 51 | } 52 | } 53 | 54 | interface ListModelsResponse { 55 | object: 'list' 56 | data: { 57 | id: string 58 | object: 'model' 59 | created: number 60 | owned_by: string 61 | }[] 62 | } 63 | 64 | export async function fetchRemoteModels(params: { apiHost: string; apiKey: string; useProxy?: boolean }) { 65 | const response = await apiRequest.get( 66 | `${params.apiHost}/models`, 67 | { 68 | Authorization: `Bearer ${params.apiKey}`, 69 | }, 70 | { 71 | useProxy: params.useProxy, 72 | } 73 | ) 74 | const json: ListModelsResponse = await response.json() 75 | if (!json.data) { 76 | throw new ApiError(JSON.stringify(json)) 77 | } 78 | return json.data.map((item) => item.id) 79 | } 80 | -------------------------------------------------------------------------------- /src/renderer/packages/models/perplexity.ts: -------------------------------------------------------------------------------- 1 | import { createPerplexity } from '@ai-sdk/perplexity' 2 | import { extractReasoningMiddleware, wrapLanguageModel } from 'ai' 3 | import AbstractAISDKModel from './abstract-ai-sdk' 4 | import { ModelHelpers } from './types' 5 | 6 | const helpers: ModelHelpers = { 7 | isModelSupportVision: (model: string) => { 8 | return false 9 | }, 10 | isModelSupportToolUse: (model: string) => { 11 | return false 12 | }, 13 | } 14 | 15 | interface Options { 16 | perplexityApiKey: string 17 | perplexityModel: string 18 | temperature?: number 19 | topP?: number 20 | } 21 | 22 | export default class Perplexity extends AbstractAISDKModel { 23 | public name = 'Perplexity API' 24 | public static helpers = helpers 25 | 26 | constructor(public options: Options) { 27 | super() 28 | } 29 | 30 | protected getChatModel() { 31 | const provider = createPerplexity({ 32 | apiKey: this.options.perplexityApiKey, 33 | }) 34 | return wrapLanguageModel({ 35 | model: provider.languageModel(this.options.perplexityModel), 36 | middleware: extractReasoningMiddleware({ tagName: 'think' }), 37 | }) 38 | } 39 | 40 | isSupportToolUse() { 41 | return helpers.isModelSupportToolUse(this.options.perplexityModel) 42 | } 43 | } 44 | 45 | export const perplexityModels = ['sonar-deep-research', 'sonar-reasoning-pro', 'sonar-reasoning', 'sonar-pro', 'sonar'] 46 | -------------------------------------------------------------------------------- /src/renderer/packages/models/types.ts: -------------------------------------------------------------------------------- 1 | import { ToolSet } from 'ai' 2 | import { Message, MessageContentParts, StreamTextResult } from 'src/shared/types' 3 | 4 | export interface ModelHelpers { 5 | isModelSupportVision(model: string): boolean 6 | isModelSupportToolUse(model: string): boolean 7 | } 8 | 9 | export interface ModelInterface { 10 | name: string 11 | isSupportToolUse(): boolean 12 | isSupportSystemMessage(): boolean 13 | chat: (messages: Message[], options: CallChatCompletionOptions) => Promise 14 | paint: (prompt: string, num: number, callback?: (picBase64: string) => any, signal?: AbortSignal) => Promise 15 | } 16 | 17 | export interface CallChatCompletionOptions { 18 | signal?: AbortSignal 19 | onResultChange?: onResultChange 20 | tools?: Tools 21 | } 22 | 23 | export interface ResultChange { 24 | // webBrowsing?: MessageWebBrowsing 25 | reasoningContent?: string 26 | // toolCalls?: MessageToolCalls 27 | contentParts?: MessageContentParts 28 | tokenCount?: number // 当前消息的 token 数量 29 | tokensUsed?: number // 生成当前消息的 token 使用量 30 | } 31 | 32 | export type onResultChangeWithCancel = (data: ResultChange & { cancel?: () => void }) => void 33 | export type onResultChange = (data: ResultChange) => void 34 | export type OnResultChangeWithCancel = onResultChangeWithCancel 35 | export type OnResultChange = onResultChange 36 | -------------------------------------------------------------------------------- /src/renderer/packages/models/xai.ts: -------------------------------------------------------------------------------- 1 | import { ContextWindowSize } from 'src/shared/constants' 2 | import { ModelHelpers } from './types' 3 | import OpenAICompatible from './openai-compatible' 4 | 5 | // https://x.ai/api#pricing 6 | const modelConfig = { 7 | 'grok-3-beta': { 8 | contextWindow: ContextWindowSize.t128k, 9 | vision: false, 10 | }, 11 | 'grok-3-mini-beta': { 12 | contextWindow: ContextWindowSize.t128k, 13 | vision: false, 14 | }, 15 | 'grok-2-vision-1212': { 16 | contextWindow: 8192, 17 | vision: true, 18 | }, 19 | 'grok-2-image-1212': { 20 | contextWindow: ContextWindowSize.t128k, 21 | vision: false, 22 | }, 23 | 'grok-2-1212': { 24 | contextWindow: ContextWindowSize.t128k, 25 | vision: false, 26 | }, 27 | 'grok-vision-beta': { 28 | contextWindow: 8192, 29 | vision: true, 30 | }, 31 | 'grok-beta': { 32 | contextWindow: ContextWindowSize.t128k, 33 | vision: false, 34 | }, 35 | } 36 | 37 | export const xAIModels = Object.keys(modelConfig) 38 | 39 | const helpers: ModelHelpers = { 40 | isModelSupportVision: (model: string) => { 41 | return model.includes('vision') 42 | }, 43 | isModelSupportToolUse: (model: string) => { 44 | return true 45 | }, 46 | } 47 | 48 | interface Options { 49 | xAIKey: string 50 | xAIModel: string 51 | temperature?: number 52 | topP?: number 53 | } 54 | 55 | export default class XAI extends OpenAICompatible { 56 | public name = 'xAI' 57 | public static helpers = helpers 58 | 59 | constructor(public options: Options) { 60 | super({ 61 | apiKey: options.xAIKey, 62 | apiHost: 'https://api.x.ai/v1', 63 | model: options.xAIModel, 64 | temperature: options.temperature, 65 | topP: options.topP, 66 | }) 67 | } 68 | 69 | isSupportToolUse() { 70 | return helpers.isModelSupportToolUse(this.options.xAIModel) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/renderer/packages/navigator.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react' 2 | import copyToClipboardFallback from 'copy-to-clipboard' 3 | 4 | export function copyToClipboard(text: string) { 5 | try { 6 | navigator?.clipboard?.writeText(text) 7 | } catch (e) { 8 | Sentry.captureException(e) 9 | } 10 | try { 11 | copyToClipboardFallback(text) 12 | } catch (e) { 13 | Sentry.captureException(e) 14 | } 15 | } 16 | 17 | const ua = navigator.userAgent 18 | 19 | export const getBrowser = (): 'Opera' | 'Chrome' | 'Firefox' | 'Safari' | 'IE' | 'Edge' | 'Unknown' | undefined => { 20 | if (ua.indexOf('Opera') > -1) { 21 | return 'Opera' 22 | } 23 | if (ua.indexOf('Chrome') > -1) { 24 | return 'Chrome' 25 | } 26 | if (ua.indexOf('Firefox') > -1) { 27 | return 'Firefox' 28 | } 29 | if (ua.indexOf('Safari') > -1) { 30 | return 'Safari' 31 | } 32 | if (ua.indexOf('MSIE') > -1) { 33 | return 'IE' 34 | } 35 | if (ua.indexOf('Trident') > -1) { 36 | return 'IE' 37 | } 38 | if (ua.indexOf('Edge') > -1) { 39 | return 'Edge' 40 | } 41 | return 'Unknown' 42 | } 43 | 44 | export const getOS = (): 'Windows' | 'Mac' | 'Linux' | 'Android' | 'iOS' | 'Unknown' => { 45 | if (ua.indexOf('Windows') > -1) { 46 | return 'Windows' 47 | } 48 | if (ua.indexOf('Mac') > -1) { 49 | return 'Mac' 50 | } 51 | if (ua.indexOf('Linux') > -1) { 52 | return 'Linux' 53 | } 54 | if (ua.indexOf('Android') > -1) { 55 | return 'Android' 56 | } 57 | if (ua.indexOf('iPhone') > -1) { 58 | return 'iOS' 59 | } 60 | if (ua.indexOf('iPad') > -1) { 61 | return 'iOS' 62 | } 63 | if (ua.indexOf('iPod') > -1) { 64 | return 'iOS' 65 | } 66 | return 'Unknown' 67 | } 68 | -------------------------------------------------------------------------------- /src/renderer/packages/token.tsx: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react' 2 | import { Tiktoken } from 'js-tiktoken/lite' 3 | // @ts-ignore 4 | import cl100k_base from 'js-tiktoken/ranks/cl100k_base' 5 | import { getMessageText } from '@/utils/message' 6 | import { Message } from '../../shared/types' 7 | 8 | const encoding = new Tiktoken(cl100k_base) 9 | function estimateTokens(str: string): number { 10 | str = typeof str === 'string' ? str : JSON.stringify(str) 11 | const tokens = encoding.encode(str) 12 | return tokens.length 13 | } 14 | 15 | // 参考: https://github.com/pkoukk/tiktoken-go#counting-tokens-for-chat-api-calls 16 | // OpenAI Cookbook: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb 17 | export function estimateTokensFromMessages(messages: Message[]) { 18 | try { 19 | const tokensPerMessage = 3 20 | const tokensPerName = 1 21 | let ret = 0 22 | for (const msg of messages) { 23 | ret += tokensPerMessage 24 | ret += estimateTokens(getMessageText(msg)) 25 | ret += estimateTokens(msg.role) 26 | if (msg.name) { 27 | ret += estimateTokens(msg.name) 28 | ret += tokensPerName 29 | } 30 | } 31 | ret += 3 // every reply is primed with <|start|>assistant<|message|> 32 | return ret 33 | } catch (e) { 34 | Sentry.captureException(e) 35 | return -1 36 | } 37 | } 38 | 39 | export function sliceTextByTokenLimit(text: string, limit: number) { 40 | let ret = '' 41 | let retTokenCount = 0 42 | const STEP_LEN = 100 43 | while (text.length > 0) { 44 | const part = text.slice(0, STEP_LEN) 45 | text = text.slice(STEP_LEN) 46 | const partTokenCount = estimateTokens(part) 47 | if (retTokenCount + partTokenCount > limit) { 48 | break 49 | } 50 | ret += part 51 | retTokenCount += partTokenCount 52 | } 53 | return ret 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/packages/web-search/base.ts: -------------------------------------------------------------------------------- 1 | import platform from '@/platform' 2 | import { CapacitorHttp } from '@capacitor/core' 3 | import { FetchOptions, ofetch } from 'ofetch' 4 | import type { SearchResult } from 'src/shared/types' 5 | 6 | abstract class WebSearch { 7 | abstract search(query: string, signal?: AbortSignal): Promise 8 | 9 | async fetch(url: string, options: FetchOptions) { 10 | const { origin } = new URL(url) 11 | if (platform.type === 'mobile') { 12 | const { data } = await CapacitorHttp.request({ 13 | url, 14 | method: options.method, 15 | headers: { 16 | ...(options.headers || ({} as any)), 17 | 'User-Agent': 18 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3', 19 | origin, 20 | referer: origin, 21 | }, 22 | params: options.query, 23 | data: options.body, 24 | }) 25 | 26 | return data 27 | } else { 28 | return ofetch(url, options) 29 | } 30 | } 31 | } 32 | 33 | export default WebSearch 34 | -------------------------------------------------------------------------------- /src/renderer/packages/web-search/bing-news.ts: -------------------------------------------------------------------------------- 1 | import WebSearch, { SearchResult } from './base' 2 | 3 | export class BingNewsSearch extends WebSearch { 4 | async search(query: string, signal?: AbortSignal): Promise { 5 | const html = await this.fetchSerp(query, signal) 6 | const items = this.extractItems(html) 7 | return { items } 8 | } 9 | 10 | private async fetchSerp(query: string, signal?: AbortSignal) { 11 | const html = await this.fetch('https://www.bing.com/news/infinitescrollajax', { 12 | method: 'GET', 13 | query: { InfiniteScroll: '1', q: query }, 14 | signal, 15 | }) 16 | return html as string 17 | } 18 | 19 | private extractItems(html: string) { 20 | const dom = new DOMParser().parseFromString(html, 'text/html') 21 | const nodes = dom.querySelectorAll('.newsitem') 22 | return Array.from(nodes) 23 | .slice(0, 10) 24 | .map((node) => { 25 | const nodeA = node.querySelector('.title')! 26 | const link = nodeA.getAttribute('href')! 27 | const title = nodeA.textContent || '' 28 | const nodeAbstract = node.querySelector('.snippet') 29 | const snippet = nodeAbstract?.textContent || '' 30 | return { title, link, snippet } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/packages/web-search/bing.ts: -------------------------------------------------------------------------------- 1 | import WebSearch, { SearchResult } from './base' 2 | 3 | export class BingSearch extends WebSearch { 4 | async search(query: string, signal?: AbortSignal): Promise { 5 | const html = await this.fetchSerp(query, signal) 6 | const items = this.extractItems(html) 7 | return { items } 8 | } 9 | 10 | private async fetchSerp(query: string, signal?: AbortSignal) { 11 | const html = await this.fetch('https://www.bing.com/search', { 12 | method: 'GET', 13 | query: { q: query }, 14 | signal, 15 | }) 16 | return html as string 17 | } 18 | 19 | private extractItems(html: string) { 20 | // TODO: .zci-wrapper 21 | const dom = new DOMParser().parseFromString(html, 'text/html') 22 | const nodes = dom.querySelectorAll('#b_results>li.b_algo') 23 | return Array.from(nodes) 24 | .slice(0, 10) 25 | .map((node) => { 26 | const nodeA = node.querySelector('h2>a')! 27 | const link = nodeA.getAttribute('href')! 28 | const title = nodeA.textContent || '' 29 | const nodeAbstract = node.querySelector('p[class^="b_lineclamp"]') 30 | const snippet = nodeAbstract?.textContent || '' 31 | return { title, link, snippet } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/packages/web-search/chatbox-search.ts: -------------------------------------------------------------------------------- 1 | import WebSearch, { SearchResult } from './base' 2 | import { webBrowsing } from '@/packages/remote' 3 | 4 | export class ChatboxSearch extends WebSearch { 5 | private licenseKey: string 6 | 7 | constructor(licenseKey: string) { 8 | super() 9 | this.licenseKey = licenseKey 10 | } 11 | 12 | async search(query: string): Promise { 13 | if (this.licenseKey) { 14 | const res = await webBrowsing({ 15 | licenseKey: this.licenseKey, 16 | query, 17 | }) 18 | 19 | return { 20 | items: res.links.map((link) => ({ 21 | title: link.title, 22 | link: link.url, 23 | snippet: link.content, 24 | })), 25 | } 26 | } else { 27 | return { 28 | items: [], 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/packages/web-search/duckduckgo.ts: -------------------------------------------------------------------------------- 1 | import WebSearch, { SearchResult } from './base' 2 | 3 | export class DuckDuckGoSearch extends WebSearch { 4 | async search(query: string, signal?: AbortSignal): Promise { 5 | const html = await this.fetchSerp(query, signal) 6 | const items = this.extractItems(html) 7 | return { items } 8 | } 9 | 10 | private async fetchSerp(query: string, signal?: AbortSignal) { 11 | const html = await this.fetch('https://html.duckduckgo.com/html/', { 12 | method: 'POST', 13 | body: new URLSearchParams({ q: query, df: 'y' }), 14 | signal, 15 | }) 16 | return html as string 17 | } 18 | 19 | private extractItems(html: string) { 20 | // TODO: .zci-wrapper 21 | const dom = new DOMParser().parseFromString(html, 'text/html') 22 | const nodes = dom.querySelectorAll('.results_links') 23 | return Array.from(nodes) 24 | .slice(0, 10) 25 | .map((node) => { 26 | const nodeA = node.querySelector('.result__a')! 27 | const link = nodeA.getAttribute('href')! 28 | const title = nodeA.textContent || '' 29 | const nodeAbstract = node.querySelector('.result__snippet') 30 | const snippet = nodeAbstract?.textContent || '' 31 | return { title, link, snippet } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/packages/web-search/tavily.ts: -------------------------------------------------------------------------------- 1 | import { ofetch } from 'ofetch' 2 | import WebSearch, { SearchResult } from './base' 3 | 4 | export class TavilySearch extends WebSearch { 5 | private apiKey: string 6 | 7 | constructor(apiKey: string) { 8 | super() 9 | this.apiKey = apiKey 10 | } 11 | 12 | async search(query: string, signal?: AbortSignal): Promise { 13 | try { 14 | const response = await ofetch('https://api.tavily.com/search', { 15 | method: 'POST', 16 | headers: { 17 | 'Content-Type': 'application/json', 18 | Authorization: `Bearer ${this.apiKey}`, 19 | }, 20 | body: { 21 | query, 22 | search_depth: 'basic', 23 | include_domains: [], 24 | exclude_domains: [], 25 | }, 26 | signal, 27 | }) 28 | 29 | const items = (response.results || []).map((result: any) => ({ 30 | title: result.title, 31 | link: result.url, 32 | snippet: result.content, 33 | })) 34 | 35 | return { items } 36 | } catch (error) { 37 | console.error('Tavily search error:', error) 38 | return { items: [] } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/packages/word-count.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react' 2 | 3 | /** 4 | * Word Count 5 | * 6 | * Word count in respect of CJK characters. 7 | * 8 | * Copyright (c) 2015 - 2016 by Hsiaoming Yang. 9 | * 10 | * https://github.com/yuehu/word-count 11 | */ 12 | var pattern = 13 | /[a-zA-Z0-9_\u0392-\u03c9\u00c0-\u00ff\u0600-\u06ff\u0400-\u04ff]+|[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g 14 | 15 | export function countWord(data: string): number { 16 | try { 17 | data = typeof data === 'string' ? data : JSON.stringify(data) 18 | var m = data.match(pattern) 19 | var count = 0 20 | if (!m) { 21 | return 0 22 | } 23 | for (var i = 0; i < m.length; i++) { 24 | if (m[i].charCodeAt(0) >= 0x4e00) { 25 | count += m[i].length 26 | } else { 27 | count += 1 28 | } 29 | } 30 | return count 31 | } catch (e) { 32 | Sentry.captureException(e) 33 | return -1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/pages/CleanWindow.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Dialog, DialogContent, DialogActions, DialogTitle, DialogContentText } from '@mui/material' 2 | import { useTranslation } from 'react-i18next' 3 | import * as atoms from '../stores/atoms' 4 | import { useAtom } from 'jotai' 5 | import * as sessionActions from '../stores/sessionActions' 6 | import { trackingEvent } from '@/packages/event' 7 | 8 | interface Props {} 9 | 10 | // 清空会话窗口 11 | export default function CleanWindow(props: Props) { 12 | const [sessionClean, setSessionClean] = useAtom(atoms.sessionCleanDialogAtom) 13 | const { t } = useTranslation() 14 | const close = () => { 15 | setSessionClean(null) 16 | } 17 | const clean = () => { 18 | if (!sessionClean) { 19 | return 20 | } 21 | sessionClean.messages.forEach((msg) => { 22 | msg?.cancel?.() 23 | }) 24 | sessionActions.clear(sessionClean.id) 25 | trackingEvent('clear_conversation', { event_category: 'user' }) 26 | close() 27 | } 28 | return ( 29 | 30 | {t('clean')} 31 | 32 | 33 | {t('delete confirmation', { 34 | sessionName: '"' + sessionClean?.name + '"', 35 | })} 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/platform/index.ts: -------------------------------------------------------------------------------- 1 | import DesktopPlatform from './desktop_platform' 2 | import { Platform } from './interfaces' 3 | import WebPlatform from './web_platform' 4 | 5 | function initPlatform(): Platform { 6 | if (window.electronAPI) { 7 | return new DesktopPlatform(window.electronAPI) 8 | } else { 9 | return new WebPlatform() 10 | } 11 | } 12 | 13 | export default initPlatform() 14 | -------------------------------------------------------------------------------- /src/renderer/platform/web_exporter.ts: -------------------------------------------------------------------------------- 1 | import { Exporter } from './interfaces' 2 | import * as base64 from '@/packages/base64' 3 | 4 | export default class WebExporter implements Exporter { 5 | constructor() {} 6 | 7 | async exportBlob(filename: string, blob: Blob, encoding?: 'utf8' | 'ascii' | 'utf16') { 8 | var eleLink = document.createElement('a') 9 | eleLink.download = filename 10 | eleLink.style.display = 'none' 11 | eleLink.href = URL.createObjectURL(blob) 12 | document.body.appendChild(eleLink) 13 | eleLink.click() 14 | document.body.removeChild(eleLink) 15 | } 16 | 17 | async exportTextFile(filename: string, content: string) { 18 | var eleLink = document.createElement('a') 19 | eleLink.download = filename 20 | eleLink.style.display = 'none' 21 | var blob = new Blob([content]) 22 | eleLink.href = URL.createObjectURL(blob) 23 | document.body.appendChild(eleLink) 24 | eleLink.click() 25 | document.body.removeChild(eleLink) 26 | } 27 | 28 | async exportImageFile(basename: string, base64Data: string) { 29 | // 解析 base64 数据 30 | let { type, data } = base64.parseImage(base64Data) 31 | if (type === '') { 32 | type = 'image/png' 33 | data = base64Data 34 | } 35 | const ext = (type.split('/')[1] || 'png').split('+')[0] // 处理 svg+xml 的情况 36 | const filename = basename + '.' + ext 37 | 38 | const raw = window.atob(data) 39 | const rawLength = raw.length 40 | const uInt8Array = new Uint8Array(rawLength) 41 | for (let i = 0; i < rawLength; ++i) { 42 | uInt8Array[i] = raw.charCodeAt(i) 43 | } 44 | const blob = new Blob([uInt8Array], { type }) 45 | var eleLink = document.createElement('a') 46 | eleLink.download = filename 47 | eleLink.style.display = 'none' 48 | eleLink.href = URL.createObjectURL(blob) 49 | document.body.appendChild(eleLink) 50 | eleLink.click() 51 | document.body.removeChild(eleLink) 52 | } 53 | 54 | async exportByUrl(filename: string, url: string) { 55 | var eleLink = document.createElement('a') 56 | eleLink.style.display = 'none' 57 | eleLink.download = filename 58 | eleLink.href = url 59 | document.body.appendChild(eleLink) 60 | eleLink.click() 61 | document.body.removeChild(eleLink) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/renderer/platform/web_platform_utils.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | import platform from '@/platform' 3 | import * as remote from '../packages/remote' 4 | import { isTextFilePath } from 'src/shared/file-extensions' 5 | 6 | export async function parseTextFileLocally(file: File): Promise<{ text: string; isSupported: boolean }> { 7 | if (!isTextFilePath(file.name)) { 8 | // 只在桌面端有 attachment.path,网页版本只有 attachment.name 9 | return { text: '', isSupported: false } 10 | } 11 | const text = await file.text() 12 | return { text, isSupported: true } 13 | } 14 | 15 | export async function parseUrlContentFree(url: string) { 16 | const result = await remote.parseUserLinkFree({ url }) 17 | const key = `parseUrl-` + uuidv4() 18 | await platform.setStoreBlob(key, result.text) 19 | return { key, title: result.title } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/preload.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronIPC } from '../shared/electron-types' 2 | 3 | declare global { 4 | // eslint-disable-next-line no-unused-vars 5 | interface Window { 6 | electronAPI?: ElectronIPC 7 | } 8 | } 9 | 10 | export {} 11 | -------------------------------------------------------------------------------- /src/renderer/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals' 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry) 7 | getFID(onPerfEntry) 8 | getFCP(onPerfEntry) 9 | getLCP(onPerfEntry) 10 | getTTFB(onPerfEntry) 11 | }) 12 | } 13 | } 14 | 15 | export default reportWebVitals 16 | -------------------------------------------------------------------------------- /src/renderer/router.tsx: -------------------------------------------------------------------------------- 1 | import { createHashHistory, createRouter, useNavigate } from '@tanstack/react-router' 2 | import { routeTree } from './routeTree.gen' 3 | import platform from './platform' 4 | 5 | // Create a new router instance 6 | export const router = createRouter({ 7 | routeTree, 8 | defaultNotFoundComponent: () => { 9 | const navigate = useNavigate() 10 | navigate({ to: '/', replace: true }) // 重定向到首页 11 | return null 12 | }, 13 | history: platform.type === 'web' ? undefined : createHashHistory(), 14 | }) 15 | 16 | // Register the router instance for type safety 17 | declare module '@tanstack/react-router' { 18 | interface Register { 19 | router: typeof router 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { currentSessionAtom } from '@/stores/atoms' 2 | import { createFileRoute, useNavigate } from '@tanstack/react-router' 3 | import { useAtomValue } from 'jotai' 4 | import { useEffect } from 'react' 5 | import icon from '@/static/icon.png' 6 | import { Typography } from '@mui/material' 7 | import Page from '@/components/Page' 8 | export const Route = createFileRoute('/')({ 9 | component: Index, 10 | }) 11 | 12 | function Index() { 13 | const navigate = useNavigate() 14 | const currentSession = useAtomValue(currentSessionAtom) 15 | 16 | useEffect(() => { 17 | if (!currentSession) { 18 | return 19 | } 20 | navigate({ 21 | to: `/session/${currentSession?.id}`, 22 | replace: true, 23 | }) 24 | }, [currentSession]) 25 | 26 | return ( 27 | 28 |
29 | 30 | Chatbox 31 |
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/routes/settings/hotkeys.tsx: -------------------------------------------------------------------------------- 1 | import { ShortcutConfig } from '@/components/Shortcut' 2 | import { useSettings } from '@/hooks/useSettings' 3 | import { Box } from '@mantine/core' 4 | import { createFileRoute } from '@tanstack/react-router' 5 | 6 | export const Route = createFileRoute('/settings/hotkeys')({ 7 | component: RouteComponent, 8 | }) 9 | 10 | function RouteComponent() { 11 | const { settings, setSettings } = useSettings() 12 | return ( 13 | 14 | setSettings({ shortcuts })} /> 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/routes/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { useIsSmallScreen } from '@/hooks/useScreenChange' 2 | import { createFileRoute, useNavigate } from '@tanstack/react-router' 3 | import { useEffect } from 'react' 4 | 5 | export const Route = createFileRoute('/settings/')({ 6 | component: RouteComponent, 7 | }) 8 | 9 | function RouteComponent() { 10 | const isSmallScreen = useIsSmallScreen() 11 | const navigate = useNavigate() 12 | useEffect(() => { 13 | if (!isSmallScreen) { 14 | navigate({ to: '/settings/provider', replace: true }) 15 | } 16 | }, [isSmallScreen]) 17 | 18 | return null 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/routes/settings/provider/index.tsx: -------------------------------------------------------------------------------- 1 | import { useIsSmallScreen } from '@/hooks/useScreenChange' 2 | import { createFileRoute, useNavigate } from '@tanstack/react-router' 3 | import { useEffect } from 'react' 4 | 5 | export const Route = createFileRoute('/settings/provider/')({ 6 | component: RouteComponent, 7 | }) 8 | 9 | function RouteComponent() { 10 | const isSmallScreen = useIsSmallScreen() 11 | const navigate = useNavigate() 12 | useEffect(() => { 13 | if (!isSmallScreen) { 14 | navigate({ to: '/settings/provider/chatbox-ai', replace: true }) 15 | } 16 | }, [isSmallScreen]) 17 | 18 | return null 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/setup/ga_init.ts: -------------------------------------------------------------------------------- 1 | import platforms from '@/platform' 2 | ;(() => { 3 | try { 4 | platforms.initTracking() 5 | } catch (e) { 6 | console.error(e) 7 | } 8 | })() 9 | -------------------------------------------------------------------------------- /src/renderer/setup/init_data.ts: -------------------------------------------------------------------------------- 1 | import { initSessionsIfNeeded } from '../stores/sessionStorageMutations' 2 | 3 | export async function initData() { 4 | await initSessionsIfNeeded() 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/setup/load_polyfill.ts: -------------------------------------------------------------------------------- 1 | import { CHATBOX_BUILD_TARGET } from '../variables' 2 | 3 | if (CHATBOX_BUILD_TARGET === 'mobile_app') { 4 | require('core-js/actual') 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/setup/mobile_safe_area.ts: -------------------------------------------------------------------------------- 1 | // 这个库解决了移动端异形屏的显示安全区域的问题,比如iPhoneX,iPhone11等 2 | // 这个库引入后,将设置全局的css变量 --mobile-safe-area-inset-top, --mobile-safe-area-inset-bottom, --mobile-safe-area-inset-left, --mobile-safe-area-inset-right 3 | // 通过这些变量,可以在css中设置安全区域的padding,margin等,来规避异形屏的显示问题 4 | // 为了达到最好的效果,在 html 的 meta 标签中设置 viewport-fit=cover 5 | 6 | import { SafeArea } from 'capacitor-plugin-safe-area' 7 | import { Keyboard } from '@capacitor/keyboard' 8 | 9 | SafeArea.getSafeAreaInsets().then(({ insets }) => { 10 | for (const [key, value] of Object.entries(insets)) { 11 | document.documentElement.style.setProperty(`--mobile-safe-area-inset-${key}`, `${value}px`) 12 | } 13 | }) 14 | 15 | SafeArea.getStatusBarHeight().then(({ statusBarHeight }) => { 16 | // console.log(statusBarHeight, 'statusbarHeight'); 17 | }) 18 | ;(async () => { 19 | // when safe-area changed 20 | const eventListener = await SafeArea.addListener('safeAreaChanged', (data) => { 21 | const { insets } = data 22 | for (const [key, value] of Object.entries(insets)) { 23 | document.documentElement.style.setProperty(`--mobile-safe-area-inset-${key}`, `${value}px`) 24 | } 25 | }) 26 | // eventListener.remove(); 27 | })() 28 | 29 | Keyboard.addListener('keyboardWillShow', async (info) => { 30 | document.documentElement.style.setProperty(`--mobile-safe-area-inset-bottom`, `0px`) 31 | }) 32 | 33 | Keyboard.addListener('keyboardWillHide', () => { 34 | SafeArea.getSafeAreaInsets().then(({ insets }) => { 35 | for (const [key, value] of Object.entries(insets)) { 36 | document.documentElement.style.setProperty(`--mobile-safe-area-inset-${key}`, `${value}px`) 37 | } 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/renderer/setup/protect.ts: -------------------------------------------------------------------------------- 1 | // 处理前端代码被剽窃的情况 2 | 3 | import platform from '../platform' 4 | import { CHATBOX_BUILD_TARGET } from '../variables' 5 | 6 | switch (CHATBOX_BUILD_TARGET) { 7 | case 'mobile_app': 8 | break 9 | case 'unknown': 10 | if (platform.type === 'web') { 11 | // protect() // 迁移过程中,暂时关闭保护 12 | } 13 | break 14 | } 15 | 16 | function protect() { 17 | setInterval(() => { 18 | if (Math.random() < 0.1) { 19 | // 如果当前地址不正确,就跳转到正确地址 20 | const hostname = window.location.hostname 21 | if (hostname !== simpleDecrypt(lh) && !hostname.endsWith(simpleDecrypt(ca))) { 22 | setTimeout(toHomePage, 300) 23 | } 24 | } 25 | }, 1400) 26 | } 27 | 28 | function toHomePage() { 29 | const l = simpleDecrypt(ll) 30 | const h = simpleDecrypt(hh) 31 | ;(window as any)[l][h] = simpleDecrypt(hf) 32 | } 33 | 34 | const lh = '^_QR]]YAB' // localhost 35 | const ca = 'QXSGSZNS_\x19UGB' // chatboxai.app 36 | const hf = 'ZDFCB\x0F\x19\x1DU_UCP_JRX\x1BWBF\x18' // https://chatboxai.app/ 37 | 38 | const ll = '^_QRE\\Y\\' // location 39 | const hh = 'ZBWU' // href 40 | 41 | // 简单的映射加密算法 42 | function simpleEncrypt(text: string): string { 43 | const key = '202315626747' 44 | let result = '' 45 | for (let i = 0; i < text.length; i++) { 46 | const c = text.charCodeAt(i) ^ key.charCodeAt(i % key.length) 47 | result += String.fromCharCode(c) 48 | } 49 | return result 50 | } 51 | 52 | function simpleDecrypt(text: string): string { 53 | return simpleEncrypt(text) 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/setup/sentry_init.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react' 2 | import platform from '../platform' 3 | import { CHATBOX_BUILD_TARGET, CHATBOX_BUILD_PLATFORM, NODE_ENV } from '@/variables' 4 | ;(async () => { 5 | const settings = await platform.getSettings() 6 | if (!settings.allowReportingAndTracking) { 7 | return 8 | } 9 | 10 | const version = await platform.getVersion().catch(() => 'unknown') 11 | Sentry.init({ 12 | dsn: 'https://eca691c5e01ebfa05958fca1fcb487a9@sentry.midway.run/697', 13 | integrations: [ 14 | new Sentry.BrowserTracing({ 15 | // Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled 16 | tracePropagationTargets: ['localhost', /^https:\/\/chatboxai\.app/, /^https:\/\/chatboxapp\.xyz/], 17 | }), 18 | new Sentry.Replay(), 19 | ], 20 | environment: NODE_ENV, 21 | // Performance Monitoring 22 | sampleRate: 0.1, 23 | tracesSampleRate: 0.1, // Capture 100% of the transactions, reduce in production! 24 | // Session Replay 25 | replaysSessionSampleRate: 0.05, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. 26 | replaysOnErrorSampleRate: 0.05, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. 27 | release: version, 28 | // 设置全局标签 29 | initialScope: { 30 | tags: { 31 | platform: platform.type, 32 | app_version: version, 33 | build_target: CHATBOX_BUILD_TARGET, 34 | build_platform: CHATBOX_BUILD_PLATFORM, 35 | }, 36 | }, 37 | }) 38 | })() 39 | -------------------------------------------------------------------------------- /src/renderer/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /src/renderer/static/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | X-Frame-Options: DENY 3 | -------------------------------------------------------------------------------- /src/renderer/static/fonts/Cairo-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/fonts/Cairo-Bold.ttf -------------------------------------------------------------------------------- /src/renderer/static/fonts/Cairo-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/fonts/Cairo-Regular.ttf -------------------------------------------------------------------------------- /src/renderer/static/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | /* radius */ 7 | --chatbox-radius-none: 0rem; 8 | --chatbox-radius-xs: 0.125rem; 9 | --chatbox-radius-sm: 0.25rem; 10 | --chatbox-radius-md: 0.5rem; 11 | --chatbox-radius-lg: 1rem; 12 | --chatbox-radius-xl: 1.5rem; 13 | --chatbox-radius-xxl: 2rem; 14 | 15 | /* spacing */ 16 | --chatbox-spacing-none: 0rem; 17 | --chatbox-spacing-3xs: 0.125rem; 18 | --chatbox-spacing-xxs: 0.25rem; 19 | --chatbox-spacing-xs: 0.5rem; 20 | --chatbox-spacing-sm: 0.75rem; 21 | --chatbox-spacing-md: 1rem; 22 | --chatbox-spacing-lg: 1.25rem; 23 | --chatbox-spacing-xl: 1.5rem; 24 | --chatbox-spacing-xxl: 2rem; 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icon.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-c-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-c-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-c-sharp-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-c-sharp-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-cpp-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-cpp-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-css-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-css-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-csv-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-csv-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-golang-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-golang-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-html-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-html-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-java-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-java-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-javascript-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-javascript-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-json-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-json-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-markdown-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-markdown-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-pdf-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-pdf-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-php-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-php-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-ppt-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-ppt-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-python-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-python-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-ruby-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-ruby-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-rust-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-rust-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-shell-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-shell-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-swift-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-swift-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-typescript-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-typescript-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-word-file-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-word-file-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-xls-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-xls-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/icons8-xml-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/icons8-xml-48.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/azure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/azure.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/chatbox-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/chatbox-ai.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/chatglm-6b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/chatglm-6b.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/claude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/claude.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/deepseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/deepseek.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/gemini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/gemini.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/groq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/groq.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/lm-studio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/lm-studio.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/ollama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/ollama.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/openai.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/perplexity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/perplexity.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/siliconflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/siliconflow.png -------------------------------------------------------------------------------- /src/renderer/static/icons/providers/xAI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/icons/providers/xAI.png -------------------------------------------------------------------------------- /src/renderer/static/wechat_qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/src/renderer/static/wechat_qrcode.png -------------------------------------------------------------------------------- /src/renderer/storage/BaseStorage.ts: -------------------------------------------------------------------------------- 1 | import platform from '@/platform' 2 | 3 | export default class BaseStorage { 4 | constructor() {} 5 | 6 | public async setItem(key: string, value: T): Promise { 7 | return this.setItemNow(key, value) 8 | } 9 | 10 | public async setItemNow(key: string, value: T): Promise { 11 | return platform.setStoreValue(key, value) 12 | } 13 | 14 | // getItem 需要保证如果数据不存在,返回默认值的同时,也要将默认值写入存储 15 | public async getItem(key: string, initialValue: T): Promise { 16 | let value: any = await platform.getStoreValue(key) 17 | if (value === undefined || value === null) { 18 | value = initialValue 19 | this.setItemNow(key, value) 20 | } 21 | return value 22 | } 23 | 24 | public async removeItem(key: string): Promise { 25 | return platform.delStoreValue(key) 26 | } 27 | 28 | public async getAll(): Promise<{ [key: string]: any }> { 29 | return platform.getAllStoreValues() 30 | } 31 | 32 | public async setAll(data: { [key: string]: any }) { 33 | return platform.setAllStoreValues(data) 34 | } 35 | 36 | // TODO: 这些数据也应该实现数据导出与导入 37 | public async setBlob(key: string, value: string) { 38 | return platform.setStoreBlob(key, value) 39 | } 40 | public async getBlob(key: string): Promise { 41 | return platform.getStoreBlob(key) 42 | } 43 | public async delBlob(key: string) { 44 | return platform.delStoreBlob(key) 45 | } 46 | public async getBlobKeys(): Promise { 47 | return platform.listStoreBlobKeys() 48 | } 49 | // subscribe(key: string, callback: any, initialValue: any): Promise 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/storage/StoreStorage.ts: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash' 2 | import { v4 as uuidv4 } from 'uuid' 3 | import BaseStorage from './BaseStorage' 4 | 5 | export enum StorageKey { 6 | ChatSessions = 'chat-sessions', 7 | Configs = 'configs', 8 | Settings = 'settings', 9 | MyCopilots = 'myCopilots', 10 | ConfigVersion = 'configVersion', 11 | RemoteConfig = 'remoteConfig', 12 | ChatSessionsList = 'chat-sessions-list', 13 | ChatSessionSettings = 'chat-session-settings', 14 | PictureSessionSettings = 'picture-session-settings', 15 | } 16 | 17 | export const StorageKeyGenerator = { 18 | session(id: string) { 19 | return `session:${id}` 20 | }, 21 | picture(category: string) { 22 | return `picture:${category}:${uuidv4()}` 23 | }, 24 | file(sessionId: string, msgId: string) { 25 | return `file:${sessionId}:${msgId}:${uuidv4()}` 26 | }, 27 | } 28 | 29 | export default class StoreStorage extends BaseStorage { 30 | constructor() { 31 | super() 32 | } 33 | public async getItem(key: string, initialValue: T): Promise { 34 | let value: T = await super.getItem(key, initialValue) 35 | 36 | if (key === StorageKey.Configs && value === initialValue) { 37 | await super.setItemNow(key, initialValue) // 持久化初始生成的 uuid 38 | } 39 | 40 | return value 41 | } 42 | 43 | // 对 setItem 进行防抖,应对消息生成时频繁写入时导致的性能问题 44 | // 实际用户反馈中发现,频繁写入时,会导致内存占用过高甚至卡顿,尤其是一些安全软件会自动扫描新创建的 JSON 文件 45 | private setItemWithDebounce = debounce( 46 | (key: string, value: any) => { 47 | return super.setItemNow(key, value) 48 | }, 49 | 500, // 这里设置太大会可能导致用户关闭应用时没有及时保存数据,根据消息生成的最大速度 100ms 设计 50 | { maxWait: 60 * 1000 } 51 | ) 52 | 53 | /** 54 | * 异步写入(防抖) 55 | * @deprecated 此方法仅用于兼容 jotail 的 atomWithStorage 的写入,不建议直接使用 56 | */ 57 | public async setItem(key: string, value: T): Promise { 58 | return this.setItemWithDebounce(key, value) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/renderer/storage/index.ts: -------------------------------------------------------------------------------- 1 | import StoreStorage, { StorageKey } from './StoreStorage' 2 | 3 | const storage = new StoreStorage() 4 | 5 | export default storage 6 | export { StorageKey } 7 | -------------------------------------------------------------------------------- /src/renderer/stores/atoms/configAtoms.ts: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from 'jotai/utils' 2 | import storage, { StorageKey } from '../../storage' 3 | 4 | // configVersion 配置版本,用于判断是否需要升级迁移配置(migration) 5 | // export const configVersionAtom = atomWithStorage(StorageKey.ConfigVersion, 0, storage) // Keep commented out if original was 6 | 7 | // 远程配置 8 | export const remoteConfigAtom = atomWithStorage<{ setting_chatboxai_first?: boolean }>( 9 | StorageKey.RemoteConfig, 10 | {}, 11 | storage 12 | ) -------------------------------------------------------------------------------- /src/renderer/stores/atoms/copilotAtoms.ts: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from 'jotai/utils' 2 | import { CopilotDetail } from '../../../shared/types' // Need this import 3 | import storage, { StorageKey } from '../../storage' 4 | import { atom } from 'jotai' // Need this import for openCopilotDialogAtom 5 | 6 | // myCopilots 7 | export const myCopilotsAtom = atomWithStorage(StorageKey.MyCopilots, [], storage) 8 | 9 | // Related UI state, moved here for proximity to copilots 10 | export const openCopilotDialogAtom = atom(false) // 是否展示copilot窗口 -------------------------------------------------------------------------------- /src/renderer/stores/atoms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './settingsAtoms' 2 | export * from './sessionAtoms' 3 | export * from './copilotAtoms' 4 | export * from './uiAtoms' 5 | export * from './configAtoms' 6 | export * from './throttleWriteSessionAtom' 7 | -------------------------------------------------------------------------------- /src/renderer/stores/atoms/uiAtoms.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react' 2 | import { atom } from 'jotai' 3 | import { atomWithStorage } from 'jotai/utils' 4 | import { Toast, MessagePicture } from '../../../shared/types' // Need this import 5 | import { VirtuosoHandle } from 'react-virtuoso' 6 | import React from 'react' // Need React for React.ReactNode 7 | 8 | // toasts 9 | export const toastsAtom = atom([]) 10 | 11 | // quote 消息引用 12 | export const quoteAtom = atom('') 13 | 14 | // theme 15 | export const realThemeAtom = atom<'light' | 'dark'>('light') // This might relate more to settings? Re-evaluating. -> Keep here for now as it might be derived/runtime theme. 16 | 17 | // message scrolling 18 | export const messageListElementAtom = atom>(null) 19 | export const messageScrollingAtom = atom>(null) 20 | export const messageScrollingAtTopAtom = atom(false) 21 | export const messageScrollingAtBottomAtom = atom(false) 22 | export const messageScrollingScrollPositionAtom = atom(0) // 当前视图高度位置(包含了视图的高度+视图距离顶部的偏移) 23 | 24 | // Sidebar visibility 25 | export const showSidebarAtom = atom(true) 26 | 27 | // Dialog states (excluding settings, session clean, copilot which were moved) 28 | export const openSearchDialogAtom = atom(false) 29 | export const openWelcomeDialogAtom = atom(false) 30 | export const openAboutDialogAtom = atom(false) // 是否展示相关信息的窗口 31 | 32 | // Input box related state 33 | export const inputBoxLinksAtom = atom<{ url: string }[]>([]) 34 | export const inputBoxWebBrowsingModeAtom = atom(false) 35 | 36 | // Picture viewer state 37 | export const pictureShowAtom = atom<{ 38 | picture: MessagePicture 39 | extraButtons?: { 40 | onClick: () => void 41 | icon: React.ReactNode 42 | }[] 43 | onSave?: () => void 44 | } | null>(null) 45 | 46 | // Layout state 47 | export const widthFullAtom = atomWithStorage('widthFull', false) // Stored UI preference -------------------------------------------------------------------------------- /src/renderer/stores/atoms/utilAtoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai' 2 | 3 | export const initLogAtom = atom([]) 4 | export const migrationProcessAtom = atom('') 5 | -------------------------------------------------------------------------------- /src/renderer/stores/queryClient.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query' 2 | 3 | export default new QueryClient() 4 | -------------------------------------------------------------------------------- /src/renderer/stores/toastActions.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultStore } from 'jotai' 2 | import * as atoms from './atoms' 3 | import { v4 as uuidv4 } from 'uuid' 4 | 5 | export function add(content: string) { 6 | const store = getDefaultStore() 7 | const newToast = { id: `toast:${uuidv4()}`, content } 8 | store.set(atoms.toastsAtom, (toasts) => [...toasts, newToast]) 9 | } 10 | 11 | export function remove(id: string) { 12 | const store = getDefaultStore() 13 | store.set(atoms.toastsAtom, (toasts) => toasts.filter((toast) => toast.id !== id)) 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/utils/image.ts: -------------------------------------------------------------------------------- 1 | import { StorageKeyGenerator } from '@/storage/StoreStorage' 2 | import storage from '@/storage' 3 | 4 | export async function saveImage(category: string, picBase64: string) { 5 | const storageKey = StorageKeyGenerator.picture(category) 6 | // 图片需要存储到 indexedDB,如果直接使用 OpenAI 返回的图片链接,图片链接将随着时间而失效 7 | await storage.setBlob(storageKey, picBase64) 8 | return storageKey 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/utils/session-utils.ts: -------------------------------------------------------------------------------- 1 | import { mapValues } from 'lodash' 2 | import { Session, SessionMeta } from 'src/shared/types' 3 | import { migrateMessage } from './message' 4 | 5 | export function migrateSession(session: Session): Session { 6 | return { 7 | ...session, 8 | messages: session.messages.map((m) => migrateMessage(m)), 9 | threads: session.threads?.map((t) => ({ 10 | ...t, 11 | messages: t.messages.map((m) => migrateMessage(m)), 12 | })), 13 | messageForksHash: mapValues(session.messageForksHash || {}, (forks) => ({ 14 | ...forks, 15 | lists: forks.lists.map((list) => ({ 16 | ...list, 17 | messages: list.messages.map((m) => migrateMessage(m)), 18 | })), 19 | })), 20 | } 21 | } 22 | 23 | export function sortSessions(sessions: SessionMeta[]): SessionMeta[] { 24 | let reversed: SessionMeta[] = [] 25 | let pinned: SessionMeta[] = [] 26 | for (const sess of sessions) { 27 | if (sess.starred) { 28 | pinned.push(sess) 29 | continue 30 | } 31 | reversed.unshift(sess) 32 | } 33 | return pinned.concat(reversed) 34 | } -------------------------------------------------------------------------------- /src/renderer/variables.ts: -------------------------------------------------------------------------------- 1 | // 在 webpack.config.base.ts 的 webpack.EnvironmentPlugin 中注册的变量, 2 | // 在编译时 webpack 会根据环境变量替换掉 process.env.XXX 3 | 4 | export const CHATBOX_BUILD_TARGET = (process.env.CHATBOX_BUILD_TARGET || 'unknown') as 'unknown' | 'mobile_app' 5 | export const CHATBOX_BUILD_PLATFORM = (process.env.CHATBOX_BUILD_PLATFORM || 'unknown') as 6 | | 'unknown' 7 | | 'ios' 8 | | 'android' 9 | | 'web' 10 | 11 | export const USE_LOCAL_API = process.env.USE_LOCAL_API || '' 12 | 13 | export const NODE_ENV = process.env.NODE_ENV || 'development' 14 | -------------------------------------------------------------------------------- /src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export enum ContextWindowSize { 2 | t16k = 16384, 3 | t32k = 32768, 4 | t64k = 65536, 5 | t128k = 131072, 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/electron-types.ts: -------------------------------------------------------------------------------- 1 | export interface ElectronIPC { 2 | invoke: (channel: string, ...args: any[]) => Promise 3 | onSystemThemeChange: (callback: () => void) => () => void 4 | onWindowShow: (callback: () => void) => () => void 5 | onUpdateDownloaded: (callback: () => void) => () => void 6 | } 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ['class'], 4 | content: ['./src/renderer/**/*.{js,jsx,ts,tsx}'], 5 | theme: { 6 | extend: { 7 | spacing: { 8 | none: 'var(--chatbox-spacing-none)', 9 | '3xs': 'var(--chatbox-spacing-3xs)', 10 | xxs: 'var(--chatbox-spacing-xxs)', 11 | xs: 'var(--chatbox-spacing-xs)', 12 | sm: 'var(--chatbox-spacing-sm)', 13 | md: 'var(--chatbox-spacing-md)', 14 | lg: 'var(--chatbox-spacing-lg)', 15 | xl: 'var(--chatbox-spacing-xl)', 16 | xxl: 'var(--chatbox-spacing-xxl)', 17 | }, 18 | borderRadius: { 19 | none: 'var(--chatbox-radius-none)', 20 | xs: 'var(--chatbox-radius-xs)', 21 | sm: 'var(--chatbox-radius-sm)', 22 | md: 'var(--chatbox-radius-md)', 23 | lg: 'var(--chatbox-radius-lg)', 24 | xl: 'var(--chatbox-radius-xl)', 25 | xxl: 'var(--chatbox-radius-xxl)', 26 | }, 27 | animation: { 28 | 'fade-in': 'fadeIn 1s ease-out', 29 | flash: 'flash 0.5s ease-in-out 2', 30 | }, 31 | keyframes: { 32 | fadeIn: { 33 | '0%': { opacity: '0' }, 34 | '100%': { opacity: '1' }, 35 | }, 36 | flash: { 37 | '0%, 100%': { opacity: '1' }, 38 | '50%': { opacity: '0.3' }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | plugins: [require('tailwindcss-animate'), require('tailwind-scrollbar')], 44 | corePlugins: { 45 | preflight: false, 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /team-sharing/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | reverse_proxy https://api.openai.com { 3 | header_up Host {http.reverse_proxy.upstream.hostport} 4 | header_up Authorization "Bearer " 5 | } 6 | } -------------------------------------------------------------------------------- /team-sharing/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM caddy:2.4.6 2 | 3 | COPY ./Caddyfile /etc/caddy/Caddyfile 4 | COPY ./main.sh /usr/src/www/main.sh 5 | 6 | RUN chmod +x /usr/src/www/main.sh 7 | 8 | ENTRYPOINT ["sh", "-c", "/usr/src/www/main.sh"] 9 | -------------------------------------------------------------------------------- /team-sharing/README-CN.md: -------------------------------------------------------------------------------- 1 | # Chatbox 团队共享功能 2 | 3 | [English](./README.md) | 中文介绍 4 | 5 | Chatbox 可以让你的团队成员共享同一个 OpenAI API 账号的资源,同时不会暴露你的 API KEY。 6 | 7 | 下面的教程将帮助你快速搭建一个共享服务器。 8 | 接下来可能涉及到服务器登录、命令行输入等操作,如果你不熟悉这些操作,可以请你的技术同事帮忙,或者询问 ChatGPT。相信我,这并不困难。 9 | 10 | ## 1. 准备一台服务器 11 | 12 | 你可以在 AWS、Google Cloud、Digital Ocean、或腾讯云海外等云平台上启动一个云服务器。 13 | 请注意服务器的网络环境必须可以正常访问 `openai.com`。 14 | 15 | ## 2. 环境安装 16 | 17 | 登陆你的服务器,执行下面的命令 18 | 19 | ```shell 20 | curl -fsSL https://get.docker.com -o get-docker.sh 21 | sh get-docker.sh 22 | ``` 23 | 24 | ## 3. 启动 Chatbox 共享服务器(HTTP) 25 | 26 | - 将下面 `` 替换成你的 OpenAI API KEY。 27 | - 执行下面的命令,启动服务器。 28 | 29 | ```shell 30 | docker run -p 80:80 -p 443:443 \ 31 | -v ./caddy_config:/config -v ./caddy_data:/data \ 32 | -e KEY= \ 33 | bensdocker/chatbox-team 34 | ``` 35 | 36 | 示例: 37 | 38 | ```shell 39 | docker run -p 80:80 -p 443:443 \ 40 | -v ./caddy_config:/config -v ./caddy_data:/data \ 41 | -e KEY=sk-xxxxxxxxxxxxxxxxxxx \ 42 | bensdocker/chatbox-team 43 | ``` 44 | 45 | ## 4. 启动 Chatbox 共享服务器(HTTPS,推荐) 46 | 47 | 如果你有一个域名,那么可以使用 HTTPS 来启动服务器,这样所有的对话消息在网络传输时都以密文加密,在隐私上更安全。 48 | 49 | - 让你的域名解析到这台服务器(并等待五分钟生效); 50 | - 将下面 `` 替换成你域名; 51 | - 将下面 `` 替换成你的 OpenAI API KEY; 52 | - 执行下面的命令,启动服务器。 53 | 54 | ```shell 55 | docker run -p 80:80 -p 443:443 \ 56 | -v ./caddy_config:/config -v ./caddy_data:/data \ 57 | -e HOST= \ 58 | -e KEY= \ 59 | bensdocker/chatbox-team 60 | ``` 61 | 62 | 示例: 63 | 64 | ```shell 65 | docker run -p 80:80 -p 443:443 \ 66 | -v ./caddy_config:/config -v ./caddy_data:/data \ 67 | -e HOST=proxy.chatbox.run \ 68 | -e KEY=sk-xxxxxxxxxxxxxxxxxx \ 69 | bensdocker/chatbox-team 70 | ``` 71 | 72 | ## 5. 分享服务器地址 73 | 74 | - 如果你启动的是 HTTP,那么地址是 `http://<你的服务器IP>:80`; 75 | - 如果你启动的是 HTTPS,那么地址是 `https://<你的域名>`。 76 | 77 | 向你的团队成员分享服务器地址。他们只需要在 Chatbox 设置中的 `API Host` 中填入地址,**不需要填写 API KEY**,就可以共享 OpenAI API 资源了。 78 | 79 | ![](./demo_http.png) 80 | 81 | ![](./demo_https.png) 82 | -------------------------------------------------------------------------------- /team-sharing/demo_http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/team-sharing/demo_http.png -------------------------------------------------------------------------------- /team-sharing/demo_https.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatboxai/chatbox/3827a0456e9fbc6b58a5cd843810d8ea8233e0e0/team-sharing/demo_https.png -------------------------------------------------------------------------------- /team-sharing/main.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | if [ -z "$HOST" ] 4 | then 5 | HOST=":80" 6 | fi 7 | 8 | sed "s//$HOST/g" /etc/caddy/Caddyfile > /etc/caddy/Caddyfile.tmp 9 | mv /etc/caddy/Caddyfile.tmp /etc/caddy/Caddyfile 10 | 11 | sed "s//$KEY/g" /etc/caddy/Caddyfile > /etc/caddy/Caddyfile.tmp 12 | mv /etc/caddy/Caddyfile.tmp /etc/caddy/Caddyfile 13 | 14 | 15 | caddy run --config /etc/caddy/Caddyfile --adapter caddyfile -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2021", 5 | "module": "commonjs", 6 | "lib": ["dom", "es2021"], 7 | "jsx": "react-jsx", 8 | "strict": true, 9 | "sourceMap": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["./src/renderer/*"] 13 | }, 14 | "moduleResolution": "node", 15 | "esModuleInterop": true, 16 | "allowSyntheticDefaultImports": true, 17 | "resolveJsonModule": true, 18 | "allowJs": true, 19 | "outDir": ".erb/dll", 20 | "skipLibCheck": true 21 | }, 22 | "exclude": ["node_modules", "dist", "tmp", "test", "release", ".erb/dll", "ios", "android"] 23 | } 24 | --------------------------------------------------------------------------------