├── .better-commits.json ├── .editorconfig ├── .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 │ ├── remove-locales.js │ ├── remove-useless.js │ └── sign.js ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .husky └── pre-commit ├── .stylelintrc.json ├── .vscode-upload.json ├── CODE_OF_CONDUCT.md ├── DEVELOPMENT.md ├── INSTALLATION.md ├── LICENSE ├── README.md ├── assets ├── assets.d.ts ├── dockicon.png ├── entitlements.mac.plist ├── fonts │ ├── JetBrainsMono.woff2 │ └── barlow400.woff2 ├── icon.icns ├── icon.ico ├── icon.png └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 512x512.png │ └── 64x64.png ├── installer.nsh ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── locales │ ├── en │ └── translation.json │ ├── zh-tw │ └── translation.json │ └── zh │ └── translation.json ├── release └── app │ ├── package-lock.json │ └── package.json ├── src ├── CrashReporter.ts ├── assets │ └── images │ │ ├── construction.dark.png │ │ ├── construction.light.png │ │ ├── design.dark.png │ │ ├── design.light.png │ │ ├── door.dark.png │ │ ├── door.light.png │ │ ├── hint.dark.png │ │ ├── hint.light.png │ │ ├── knowledge.dark.png │ │ ├── knowledge.light.png │ │ ├── reading.dark.png │ │ ├── reading.light.png │ │ ├── tools.dark.png │ │ ├── tools.light.png │ │ ├── usage.dark.png │ │ └── usage.light.png ├── consts.ts ├── d.ts ├── hooks │ ├── useECharts.ts │ ├── useLazyEffect.ts │ ├── useMarkdown.ts │ ├── useMermaid.ts │ ├── useNav.ts │ ├── useOnlineStatus.ts │ ├── useToast.tsx │ └── useToken.ts ├── intellichat │ ├── readers │ │ ├── AnthropicReader.ts │ │ ├── BaseReader.ts │ │ ├── GoogleReader.ts │ │ ├── IChatReader.ts │ │ ├── OllamaChatReader.ts │ │ └── OpenAIReader.ts │ ├── services │ │ ├── AnthropicChatService.ts │ │ ├── AzureChatService.ts │ │ ├── BaiduChatService.ts │ │ ├── DeepSeekChatService.ts │ │ ├── DoubaoChatService.ts │ │ ├── FireChatService.ts │ │ ├── GoogleChatService.ts │ │ ├── GrokChatService.ts │ │ ├── IChatService.ts │ │ ├── INextCharService.ts │ │ ├── LMStudioChatService.ts │ │ ├── MistralChatService.ts │ │ ├── MoonshotChatService.ts │ │ ├── NextChatService.ts │ │ ├── OllamaChatService.ts │ │ ├── OpenAIChatService.ts │ │ └── index.ts │ ├── types.ts │ └── validators.ts ├── libs │ └── markdownit-plugins │ │ ├── CodeCopy.ts │ │ └── markdownItEChartsPlugin.ts ├── main │ ├── crypt.ts │ ├── docloader.ts │ ├── downloader.ts │ ├── embedder.ts │ ├── knowledge.ts │ ├── logging.ts │ ├── main.ts │ ├── mcp.ts │ ├── menu.ts │ ├── preload.ts │ ├── sqlite.ts │ └── util.ts ├── mcp.config.ts ├── providers │ ├── Anthropic.ts │ ├── Azure.ts │ ├── Baidu.ts │ ├── DeepSeek.ts │ ├── Doubao.ts │ ├── Fire.ts │ ├── Google.ts │ ├── Grok.ts │ ├── LMStudio.ts │ ├── Mistral.ts │ ├── Moonshot.ts │ ├── Ollama.ts │ ├── OpenAI.ts │ ├── index.ts │ └── types.ts ├── renderer │ ├── App.scss │ ├── App.tsx │ ├── ChatContext.ts │ ├── apps │ │ ├── Loader.tsx │ │ ├── NotFound.tsx │ │ ├── index.ts │ │ ├── leonardo │ │ │ ├── App.tsx │ │ │ ├── app-leonardo.png │ │ │ └── index.tsx │ │ ├── sora │ │ │ ├── App.tsx │ │ │ ├── app-sora.png │ │ │ └── index.tsx │ │ └── types.ts │ ├── components │ │ ├── AlertDialog.tsx │ │ ├── Assets.tsx │ │ ├── ChatFolder.tsx │ │ ├── ChatFolders.tsx │ │ ├── ChatIcon.tsx │ │ ├── ChatItem.tsx │ │ ├── ConfirmDialog.tsx │ │ ├── Empty.tsx │ │ ├── FluentApp.tsx │ │ ├── FolderSettingsDialog.tsx │ │ ├── ListInput.tsx │ │ ├── MaskableInput.tsx │ │ ├── MaskableStateInput.tsx │ │ ├── SearchDialog.tsx │ │ ├── Spinner.scss │ │ ├── Spinner.tsx │ │ ├── StateButton.tsx │ │ ├── StateInput.tsx │ │ ├── ToolSetup.tsx │ │ ├── ToolSpinner.scss │ │ ├── ToolSpinner.tsx │ │ ├── ToolStatusIndicator.tsx │ │ ├── TooltipIcon.tsx │ │ ├── TrafficLights.tsx │ │ ├── UpgradeIndicator.tsx │ │ ├── icons │ │ │ ├── ComposioLogo.tsx │ │ │ └── HigressLogo.tsx │ │ └── layout │ │ │ ├── AppHeader.scss │ │ │ ├── AppHeader.tsx │ │ │ └── aside │ │ │ ├── AppNav.tsx │ │ │ ├── AppSidebar.scss │ │ │ ├── AppSidebar.tsx │ │ │ ├── BookmarkNav.tsx │ │ │ ├── ChatNav.tsx │ │ │ ├── Footer.tsx │ │ │ ├── GlobalNav.tsx │ │ │ └── WorkspaceMenu.tsx │ ├── fluentui.scss │ ├── i18n.ts │ ├── index.ejs │ ├── index.tsx │ ├── logging.ts │ ├── pages │ │ ├── apps │ │ │ └── index.tsx │ │ ├── bookmark │ │ │ ├── Bookmark.scss │ │ │ ├── Bookmark.tsx │ │ │ └── index.tsx │ │ ├── chat │ │ │ ├── Chat.scss │ │ │ ├── ChatSettingsDrawer.tsx │ │ │ ├── CitationDialog.tsx │ │ │ ├── Editor │ │ │ │ ├── PromptVariableDialog.tsx │ │ │ │ ├── Toolbar │ │ │ │ │ ├── CtxNumCtrl.tsx │ │ │ │ │ ├── ImgCtrl.tsx │ │ │ │ │ ├── KnowledgeCtrl.tsx │ │ │ │ │ ├── MaxTokensCtrl.tsx │ │ │ │ │ ├── ModelCtrl.tsx │ │ │ │ │ ├── PromptCtrl.tsx │ │ │ │ │ ├── StreamCtrl.tsx │ │ │ │ │ ├── TemperatureCtrl.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Header.tsx │ │ │ ├── Message.tsx │ │ │ ├── MessageToolbar.tsx │ │ │ ├── Messages.tsx │ │ │ ├── Sidebar │ │ │ │ └── Sidebar.tsx │ │ │ └── index.tsx │ │ ├── knowledge │ │ │ ├── CollectionForm.tsx │ │ │ ├── FileDrawer.tsx │ │ │ ├── Grid.tsx │ │ │ └── index.tsx │ │ ├── prompt │ │ │ ├── Form.tsx │ │ │ ├── Grid.tsx │ │ │ └── index.tsx │ │ ├── providers │ │ │ ├── CapabilityTag.tsx │ │ │ ├── ModelFormDrawer.tsx │ │ │ ├── ModelList.tsx │ │ │ ├── ProviderForm.tsx │ │ │ ├── ProviderList.tsx │ │ │ ├── ToolTag.tsx │ │ │ └── index.tsx │ │ ├── settings │ │ │ ├── AppearanceSettings.tsx │ │ │ ├── EmbedSettings.tsx │ │ │ ├── LanguageSettings.tsx │ │ │ ├── Settings.scss │ │ │ ├── Version.tsx │ │ │ └── index.tsx │ │ ├── tool │ │ │ ├── DetailDialog.tsx │ │ │ ├── Grid.tsx │ │ │ ├── InstallDialog.tsx │ │ │ ├── LocalServerEditDialog.tsx │ │ │ ├── MarketDrawer.tsx │ │ │ ├── NewButton.tsx │ │ │ ├── RemoteServerEditDialog.tsx │ │ │ └── index.tsx │ │ ├── usage │ │ │ ├── Grid.tsx │ │ │ └── index.tsx │ │ └── user │ │ │ ├── Account.tsx │ │ │ ├── Login.tsx │ │ │ ├── Register.tsx │ │ │ ├── TabPassword.tsx │ │ │ └── TabSubscription.tsx │ ├── preload.d.ts │ └── variables.scss ├── stores │ ├── useAppearanceStore.ts │ ├── useAuthStore.ts │ ├── useBookmarkStore.ts │ ├── useChatKnowledgeStore.ts │ ├── useChatStore.ts │ ├── useInspectorStore.ts │ ├── useKnowledgeStore.ts │ ├── useMCPServerMarketStore.ts │ ├── useMCPStore.ts │ ├── usePromptStore.ts │ ├── useProviderStore.ts │ ├── useSettingsStore.ts │ └── useUsageStore.ts ├── types │ ├── appearance.d.ts │ ├── auth.d.ts │ ├── bookmark.d.ts │ ├── d.ts │ ├── device.d.ts │ ├── error.d.ts │ ├── knowledge.d.ts │ ├── mcp.d.ts │ ├── settings.d.ts │ └── usage.d.ts ├── utils │ ├── bus.ts │ ├── cache.ts │ ├── mcp.ts │ ├── token.ts │ ├── util.ts │ └── validators.ts └── vendors │ ├── axiom.ts │ └── supa.ts ├── tailwind.config.js ├── test ├── assets │ ├── AI-Career.pdf │ ├── SOTA.md │ ├── 出师表.txt │ ├── 探索智慧的疆界.pptx │ ├── 演示项目.xlsx │ └── 长恨歌.docx ├── data │ └── 05-versions-space.pdf ├── intellichat │ ├── reader.spec.ts │ └── validators.spec.ts ├── main │ ├── docloader.spec.ts │ ├── embedder.spec.ts │ ├── knowledge.spec.ts │ └── util.spec.ts ├── mocks │ ├── electron-log.js │ └── electron.js └── utils │ ├── mcp.test.ts │ ├── token.test.ts │ ├── util.test.ts │ └── validators.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.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 { RsdoctorWebpackPlugin } from '@rsdoctor/webpack-plugin'; 9 | import { dependencies as externals } from '../../release/app/package.json'; 10 | 11 | const configuration: webpack.Configuration = { 12 | externals: [ 13 | ...Object.keys(externals || {}), 14 | function ({ request }, callback) { 15 | if ( 16 | request && 17 | (request.endsWith('.node') || request.includes('lancedb')) 18 | ) { 19 | return callback(null, 'commonjs ' + request); 20 | } 21 | callback(); 22 | }, 23 | ], 24 | 25 | stats: 'errors-only', 26 | 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.[jt]sx?$/, 31 | exclude: /node_modules/, 32 | use: { 33 | loader: 'ts-loader', 34 | options: { 35 | // Remove this line to enable type checking in webpack builds 36 | transpileOnly: true, 37 | compilerOptions: { 38 | module: 'NodeNext', 39 | }, 40 | }, 41 | }, 42 | }, 43 | ], 44 | }, 45 | 46 | output: { 47 | path: webpackPaths.srcPath, 48 | // https://github.com/webpack/webpack/issues/1114 49 | library: { 50 | type: 'commonjs2', 51 | }, 52 | }, 53 | 54 | /** 55 | * Determine the array of extensions that should be used to resolve modules. 56 | */ 57 | resolve: { 58 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 59 | modules: [webpackPaths.srcPath, 'node_modules'], 60 | // There is no need to add aliases here, the paths in tsconfig get mirrored 61 | plugins: [new TsconfigPathsPlugins()], 62 | }, 63 | 64 | plugins: [ 65 | new webpack.EnvironmentPlugin({ 66 | NODE_ENV: 'production', 67 | }), 68 | process.env.RSDOCTOR && 69 | new RsdoctorWebpackPlugin({ 70 | // 插件选项 71 | }), 72 | ].filter(Boolean), 73 | }; 74 | 75 | export default configuration; 76 | -------------------------------------------------------------------------------- /.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.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | const configuration: webpack.Configuration = { 19 | devtool: 'source-map', 20 | 21 | mode: 'production', 22 | 23 | target: 'electron-main', 24 | 25 | entry: { 26 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 27 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'), 28 | }, 29 | 30 | output: { 31 | path: webpackPaths.distMainPath, 32 | filename: '[name].js', 33 | library: { 34 | type: 'umd', 35 | }, 36 | }, 37 | 38 | optimization: { 39 | minimizer: [ 40 | new TerserPlugin({ 41 | parallel: true, 42 | }), 43 | ], 44 | }, 45 | 46 | plugins: [ 47 | new BundleAnalyzerPlugin({ 48 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 49 | analyzerPort: 8888, 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: 'production', 63 | DEBUG_PROD: 'false', 64 | START_MINIMIZED: 'false', 65 | }), 66 | 67 | new webpack.DefinePlugin({ 68 | 'process.type': '"browser"', 69 | }), 70 | // 与@xenova/transfomers 有冲突,暂时禁用。https://github.com/bytenode/bytenode/issues/197 71 | // new BytenodeWebpackPlugin({ 72 | // compileForElectron: true, 73 | // }), 74 | ], 75 | 76 | /** 77 | * Disables webpack processing of __dirname and __filename. 78 | * If you run the bundle in node.js it falls back to these values of node.js. 79 | * https://github.com/webpack/webpack/issues/2010 80 | */ 81 | node: { 82 | __dirname: false, 83 | __filename: false, 84 | }, 85 | }; 86 | 87 | export default merge(baseConfig, configuration); 88 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.preload.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { merge } from 'webpack-merge'; 4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 5 | import baseConfig from './webpack.config.base'; 6 | import webpackPaths from './webpack.paths'; 7 | import checkNodeEnv from '../scripts/check-node-env'; 8 | 9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 10 | // at the dev webpack config is not accidentally run in a production environment 11 | if (process.env.NODE_ENV === 'production') { 12 | checkNodeEnv('development'); 13 | } 14 | 15 | const configuration: webpack.Configuration = { 16 | devtool: 'inline-source-map', 17 | 18 | mode: 'development', 19 | 20 | target: 'electron-preload', 21 | 22 | entry: path.join(webpackPaths.srcMainPath, 'preload.ts'), 23 | 24 | output: { 25 | path: webpackPaths.dllPath, 26 | filename: 'preload.js', 27 | library: { 28 | type: 'umd', 29 | }, 30 | }, 31 | 32 | plugins: [ 33 | new BundleAnalyzerPlugin({ 34 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 35 | }), 36 | 37 | /** 38 | * Create global constants which can be configured at compile time. 39 | * 40 | * Useful for allowing different behaviour between development builds and 41 | * release builds 42 | * 43 | * NODE_ENV should be production so that modules do not perform certain 44 | * development checks 45 | * 46 | * By default, use 'development' as NODE_ENV. This can be overriden with 47 | * 'staging', for example, by changing the ENV variables in the npm scripts 48 | */ 49 | new webpack.EnvironmentPlugin({ 50 | NODE_ENV: 'development', 51 | }), 52 | 53 | new webpack.LoaderOptionsPlugin({ 54 | debug: true, 55 | }), 56 | ], 57 | 58 | /** 59 | * Disables webpack processing of __dirname and __filename. 60 | * If you run the bundle in node.js it falls back to these values of node.js. 61 | * https://github.com/webpack/webpack/issues/2010 62 | */ 63 | node: { 64 | __dirname: false, 65 | __filename: false, 66 | }, 67 | 68 | watch: true, 69 | }; 70 | 71 | export default merge(baseConfig, configuration); 72 | -------------------------------------------------------------------------------- /.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 | import path from '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/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/.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( 13 | 'The main process is not built yet. Build it by running "npm run build:main"', 14 | ), 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"', 22 | ), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.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( 29 | 'Webpack does not work with native dependencies.', 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 32 | plural ? 'are native dependencies' : 'is a native dependency' 33 | } and should be installed inside of the "./release/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":', 38 | )} 39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 40 | ${chalk.bold('Install the package to "./release/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold( 42 | 'cd ./release/app && npm install your-package', 43 | )} 44 | Read more about native dependencies at: 45 | ${chalk.bold( 46 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure', 47 | )} 48 | `); 49 | process.exit(1); 50 | } 51 | } catch (e) { 52 | console.log('Native dependencies could not be checked'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.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( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`, 12 | ), 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.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 = [ 6 | webpackPaths.distPath, 7 | webpackPaths.buildPath, 8 | webpackPaths.dllPath, 9 | ]; 10 | 11 | foldersToRemove.forEach((folder) => { 12 | if (fs.existsSync(folder)) rimrafSync(folder); 13 | }); 14 | -------------------------------------------------------------------------------- /.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 ( 7 | Object.keys(dependencies || {}).length > 0 && 8 | fs.existsSync(webpackPaths.appNodeModulesPath) 9 | ) { 10 | const electronRebuildCmd = 11 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; 12 | const cmd = 13 | process.platform === 'win32' 14 | ? electronRebuildCmd.replace(/\//g, '\\') 15 | : electronRebuildCmd; 16 | execSync(cmd, { 17 | cwd: webpackPaths.appPath, 18 | stdio: 'inherit', 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.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 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 11 | console.warn( 12 | 'Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set', 13 | ); 14 | return; 15 | } 16 | 17 | const appName = context.packager.appInfo.productFilename; 18 | console.info(`Notarizing ${build.appId} start...`); 19 | await notarize({ 20 | tool: 'notarytool', 21 | appBundleId: build.appId, 22 | appPath: `${appOutDir}/${appName}.app`, 23 | teamId: process.env.APPLE_TEAM_ID, 24 | appleId: process.env.APPLE_ID, 25 | appleIdPassword: process.env.APPLE_ID_PASS, 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /.erb/scripts/remove-locales.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const fs = require('fs'); 3 | 4 | // https://www.electron.build/configuration/configuration#afterpack 5 | exports.default = async function removeLocales(context) { 6 | const languages = ['en', 'zh_CN']; 7 | const localeDirs = `${context.appOutDir}/${ 8 | context.packager.appInfo.productName 9 | }.app/Contents/Frameworks/Electron Framework.framework/Resources/!(${languages.join( 10 | '|', 11 | )}).lproj`; 12 | console.log('After Pack, remove unused locales:', localeDirs); 13 | const res = glob.GlobSync(localeDirs); 14 | res.found.forEach((dir) => { 15 | console.log('remove locale file:', dir); 16 | fs.rmSync(dir, { recursive: true, force: true }); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /.erb/scripts/remove-useless.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | async function removeLocales(context) { 6 | const languages = ['en', 'zh_CN']; 7 | const localeDirs = path.join( 8 | context.appOutDir, 9 | `${context.packager.appInfo.productName}.app`, 10 | 'Contents', 11 | 'Frameworks', 12 | 'Electron Framework.framework', 13 | 'Resources', 14 | `!(${languages.join('|')}).lproj`, 15 | ); 16 | console.log(`\nRemove useless language files\n`); 17 | const res = glob.GlobSync(localeDirs); 18 | res.found.forEach((dir) => { 19 | console.log('Remove locale file:', dir); 20 | fs.rmSync(dir, { recursive: true, force: true }); 21 | }); 22 | } 23 | 24 | async function removeUnusedOnnxRuntime(context) { 25 | const nodeModulesDir = path.join( 26 | context.appOutDir, 27 | `${context.packager.appInfo.productName}.app`, 28 | 'Contents', 29 | 'Resources', 30 | 'app.asar.unpacked', 31 | 'node_modules', 32 | ); 33 | const onnxruntimeDir = path.join( 34 | nodeModulesDir, 35 | 'onnxruntime-node', 36 | 'bin', 37 | 'napi-v3', 38 | ); 39 | const onnxruntimeUnusedDirs = path.join( 40 | onnxruntimeDir, 41 | `!(${context.packager.platform.nodeName})`, 42 | ); 43 | console.log(`\nRemove unused onnx runtime from\n`, onnxruntimeUnusedDirs); 44 | const res = glob.GlobSync(onnxruntimeUnusedDirs); 45 | res.found.forEach((dir) => { 46 | console.log('Remove unused runtime:', dir); 47 | fs.rmSync(dir, { recursive: true, force: true }); 48 | }); 49 | } 50 | 51 | // https://www.electron.build/configuration/configuration#afterpack 52 | exports.default = async function remove(context) { 53 | console.log('After Pack, remove useless files'); 54 | await removeLocales(context); 55 | await removeUnusedOnnxRuntime(context); 56 | }; 57 | -------------------------------------------------------------------------------- /.erb/scripts/sign.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | exports.default = async function signArtifacts(context) { 4 | console.info('afterAllArtifactBuild hook triggered'); 5 | const { artifactPaths } = context; 6 | 7 | console.info(`Artifact Paths: ${JSON.stringify(artifactPaths)}`); 8 | 9 | // Check if this is a Linux build by looking for any AppImage 10 | const isLinux = artifactPaths.some((artifact) => 11 | artifact.endsWith('.AppImage'), 12 | ); 13 | if (!isLinux) { 14 | console.info('No AppImage found, skipping signing'); 15 | return; 16 | } 17 | 18 | if (!process.env.GPG_KEY_ID) { 19 | throw new Error( 20 | 'GPG_KEY_ID environment variable must be set to a valid GPG key ID (e.g., F51DE3D45EEFC1387B4469E788BBA7820E939D09)', 21 | ); 22 | } 23 | 24 | // Filter all AppImages from artifactPaths 25 | const appImages = artifactPaths.filter((artifact) => 26 | artifact.endsWith('.AppImage'), 27 | ); 28 | 29 | if (!appImages.length) { 30 | throw new Error('No AppImages found in artifact paths'); 31 | } 32 | 33 | // Sign each AppImage using forEach 34 | appImages.forEach((appImagePath) => { 35 | console.info( 36 | `Signing AppImage with key ${process.env.GPG_KEY_ID}: ${appImagePath}`, 37 | ); 38 | try { 39 | execSync( 40 | `gpg --detach-sign --armor --yes --default-key ${process.env.GPG_KEY_ID} "${appImagePath}"`, 41 | { stdio: 'inherit' }, 42 | ); 43 | console.info(`AppImage signed successfully: ${appImagePath}.asc`); 44 | } catch (error) { 45 | console.error(`Failed to sign AppImage: ${error.message}`); 46 | throw error; // This will stop the build and report the error 47 | } 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | # eslint ignores hidden directories by default: 32 | # https://github.com/eslint/eslint/issues/8429 33 | !.erb 34 | -------------------------------------------------------------------------------- /.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 | 'react/require-default-props': 'warn', 10 | 'import/extensions': 'off', 11 | 'import/no-unresolved': 'warn', 12 | 'import/no-import-module-exports': 'off', 13 | 'no-shadow': 'warn', 14 | '@typescript-eslint/no-shadow': 'error', 15 | 'no-unused-vars': 'warn', 16 | 'react-hooks/exhaustive-deps': 'warn', 17 | '@typescript-eslint/no-unused-vars': 'error', 18 | 'prefer-destructuring': 'warn', 19 | 'react/jsx-props-no-spreading': 'warn', 20 | }, 21 | parserOptions: { 22 | ecmaVersion: 2020, 23 | sourceType: 'module', 24 | project: './tsconfig.json', 25 | tsconfigRootDir: __dirname, 26 | createDefaultProgram: true, 27 | }, 28 | settings: { 29 | 'import/resolver': { 30 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 31 | node: {}, 32 | webpack: { 33 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 34 | }, 35 | typescript: {}, 36 | }, 37 | 'import/parsers': { 38 | '@typescript-eslint/parser': ['.ts', '.tsx'], 39 | }, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /.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/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Mac / Windows] 28 | - Version [e.g. 0.9.7] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | /dump.rdb 31 | /.vscode 32 | .env 33 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "selector-class-pattern": null 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode-upload.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name":"", 3 | "host": "", 4 | "port": 22, 5 | "username": "", 6 | "password": "", 7 | "remotePath": "", 8 | "localPath": "", 9 | "disable": false, 10 | "private_key": "~/.ssh/id_rsa" 11 | },{ 12 | "name":"", 13 | "host": "", 14 | "port": 22, 15 | "username": "", 16 | "password": "", 17 | "remotePath": "", 18 | "localPath": "", 19 | "disable": false, 20 | "private_key": "~/.ssh/id_rsa" 21 | }] -------------------------------------------------------------------------------- /INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # Installation Guide 2 | 3 | This guide provides detailed instructions for installing all prerequisites required to run 5ire. 4 | 5 | ## Python Installation 6 | 7 | ### Windows 8 | 1. Visit [Python's official website](https://www.python.org/downloads/) 9 | 2. Download the latest Python installer 10 | 3. Run the installer and make sure to check "Add Python to PATH" 11 | 4. Verify installation in Command Prompt: `python --version` 12 | 13 | Or using package managers: 14 | 15 | ```bash 16 | # Using winget 17 | winget install Python 18 | 19 | # Using scoop 20 | scoop install python 21 | ``` 22 | 23 | ### macOS 24 | ```bash 25 | # Using Homebrew 26 | brew install python 27 | ``` 28 | 29 | ### Linux (Ubuntu/Debian) 30 | ```bash 31 | sudo apt update 32 | sudo apt install python3 33 | ``` 34 | 35 | ## Node.js Installation 36 | 37 | ### Windows 38 | 1. Visit [Node.js website](https://nodejs.org/) 39 | 2. Download and install the LTS version 40 | 3. Verify installation: `node --version` and `npm --version` 41 | 42 | Or using package managers: 43 | 44 | ```bash 45 | # Using winget 46 | winget install OpenJS.NodeJS.LTS 47 | 48 | # Using scoop 49 | scoop install nodejs-lts 50 | ``` 51 | 52 | ### macOS 53 | ```bash 54 | # Using Homebrew 55 | brew install node 56 | ``` 57 | 58 | ### Linux (Ubuntu/Debian) 59 | ```bash 60 | curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - 61 | sudo apt install nodejs 62 | ``` 63 | 64 | ## uv Package Manager Installation 65 | 66 | Install uv using one of the following methods: 67 | 68 | ### Standalone Installers 69 | ```bash 70 | # On macOS and Linux 71 | curl -LsSf https://astral.sh/uv/install.sh | sh 72 | 73 | # On Windows 74 | powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" 75 | ``` 76 | 77 | ### Via Python Package Managers 78 | ```bash 79 | # With pip 80 | pip install uv 81 | 82 | # Or with pipx 83 | pipx install uv 84 | ``` 85 | 86 | After installation, you can update uv to the latest version: 87 | ```bash 88 | uv self update 89 | ``` 90 | 91 | Verify installation: 92 | ```bash 93 | uv --version 94 | ``` 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Open Source License 2 | 3 | 5ire is licensed under a modified version of the Apache License 2.0, with the following additional conditions: 4 | 5 | 1. Definitions 6 | 7 | "Community Edition": The original software distributed under Apache 2.0. 8 | "Commercial Use": Any use case defined in Section 3.2 requiring a paid license. 9 | "5ire Brand": Trademarks, logos, and product names owned by 5ire. 10 | 11 | 2. License Grants 12 | 13 | Community Edition: Licensed under Apache License 2.0 with Additional Terms (Section 3). 14 | Commercial Edition: Available under a separate proprietary license (contact 5ire for details). 15 | 16 | 3. Additional Terms to Apache 2.0 17 | 18 | 3.1 Permitted Use 19 | 20 | Free to use, modify, and distribute the Community Edition only if: 21 | You do not use the 5ire Brand in any modified versions (including names, logos, or marketing materials). 22 | You prominently state: "This product is based on 5ire Community Edition and is not affiliated with 5ire." 23 | 24 | 3.2 Commercial License Required 25 | 26 | You must obtain a commercial license from 5ire if: 27 | Modification + Distribution: Distribute modified code (directly or as SaaS) and meet any of: 28 | (a) Serve enterprise clients (>10 users ) 29 | (b) Integrate into hardware/products for sale. 30 | (c) Government/education procurement involving sensitive data. 31 | Brand Usage: Use the 5ire Brand in any product, service, or promotion. 32 | 33 | 3.3 Contributor Agreement 34 | 35 | By contributing code, you: 36 | Grant 5ire exclusive rights to dual-license your contributions (Apache 2.0 + proprietary). 37 | Waive any patent claims against 5ire's commercial use of your contributions. 38 | 39 | 4. Trademark Protection 40 | 41 | The 5ire Brand is excluded from the Apache 2.0 grant. Unauthorized use is prohibited. 42 | 43 | 5. Updates & Enforcement 44 | 45 | 5ire reserves the right to update these terms. Continued use after 30 days constitutes acceptance. 46 | Violations may result in termination of rights and legal action. 47 | -------------------------------------------------------------------------------- /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/dockicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/dockicon.png -------------------------------------------------------------------------------- /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/fonts/JetBrainsMono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/fonts/JetBrainsMono.woff2 -------------------------------------------------------------------------------- /assets/fonts/barlow400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/fonts/barlow400.woff2 -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/icon.png -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/assets/icons/64x64.png -------------------------------------------------------------------------------- /installer.nsh: -------------------------------------------------------------------------------- 1 | !macro customInstall 2 | DeleteRegKey HKCR "app5ire" 3 | WriteRegStr HKCR "app5ire" "" "URL:app5ire" 4 | WriteRegStr HKCR "app5ire" "URL Protocol" "" 5 | WriteRegStr HKCR "app5ire\shell" "" "" 6 | WriteRegStr HKCR "app5ire\shell\Open" "" "" 7 | WriteRegStr HKCR "app5ire\shell\Open\command" "" "$INSTDIR\{APP_EXECUTABLE_FILENAME} %1" 8 | !macroend 9 | 10 | !macro customUnInstall 11 | DeleteRegKey HKCR "app5ire" 12 | !macroend 13 | 14 | # Fix Can not find Squairrel error 15 | # https://github.com/electron-userland/electron-builder/issues/837#issuecomment-355698368 16 | !macro customInit 17 | nsExec::Exec '"$LOCALAPPDATA\5ire\Update.exe" --uninstall -s' 18 | !macroend 19 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | import tailwindcss from 'tailwindcss'; 2 | import autoprefixer from 'autoprefixer'; 3 | 4 | module.exports = { 5 | plugins: { 6 | 'tailwindcss/nesting': {}, 7 | tailwindcss, 8 | autoprefixer, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5ire", 3 | "version": "0.12.1", 4 | "description": "A Sleek Desktop AI Assistant & MCP Client", 5 | "license": "Modified Apache-2.0", 6 | "author": { 7 | "name": "Ironben", 8 | "email": "support@5ire.app", 9 | "url": "https://5ire.app" 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 | "@lancedb/lancedb": "^0.14.1", 19 | "@xenova/transformers": "^2.17.2", 20 | "apache-arrow": "^17.0.0", 21 | "better-sqlite3": "11.1.1", 22 | "electron-deeplink": "^1.0.10" 23 | }, 24 | "volta": { 25 | "node": "20.10.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CrashReporter.ts: -------------------------------------------------------------------------------- 1 | import { app, crashReporter } from 'electron'; 2 | import { debug } from './main/logging'; 3 | 4 | export default function initCrashReporter() { 5 | crashReporter.start({ 6 | productName: app.getName(), 7 | ignoreSystemCrashHandler: true, 8 | submitURL: `${process.env.SENTRY_DSN}/minidump/?sentry_key=${process.env.SENTRY_KEY}`, 9 | }); 10 | crashReporter.addExtraParameter('version', app.getVersion()); 11 | debug('CrashReporter initialized'); 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/images/construction.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/construction.dark.png -------------------------------------------------------------------------------- /src/assets/images/construction.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/construction.light.png -------------------------------------------------------------------------------- /src/assets/images/design.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/design.dark.png -------------------------------------------------------------------------------- /src/assets/images/design.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/design.light.png -------------------------------------------------------------------------------- /src/assets/images/door.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/door.dark.png -------------------------------------------------------------------------------- /src/assets/images/door.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/door.light.png -------------------------------------------------------------------------------- /src/assets/images/hint.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/hint.dark.png -------------------------------------------------------------------------------- /src/assets/images/hint.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/hint.light.png -------------------------------------------------------------------------------- /src/assets/images/knowledge.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/knowledge.dark.png -------------------------------------------------------------------------------- /src/assets/images/knowledge.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/knowledge.light.png -------------------------------------------------------------------------------- /src/assets/images/reading.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/reading.dark.png -------------------------------------------------------------------------------- /src/assets/images/reading.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/reading.light.png -------------------------------------------------------------------------------- /src/assets/images/tools.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/tools.dark.png -------------------------------------------------------------------------------- /src/assets/images/tools.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/tools.light.png -------------------------------------------------------------------------------- /src/assets/images/usage.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/usage.dark.png -------------------------------------------------------------------------------- /src/assets/images/usage.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/assets/images/usage.light.png -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | export const TEMP_CHAT_ID = 'temp'; 2 | 3 | export const EARLIEST_DATE = new Date('2023-08-01'); 4 | export const DEFAULT_PROVIDER = 'OpenAI'; 5 | export const ERROR_MODEL = 'ERROR_MODEL'; 6 | 7 | export const DEFAULT_TEMPERATURE = 0.9; 8 | 9 | export const DEFAULT_CONTEXT_WINDOW = 128000; 10 | export const MAX_CONTEXT_WINDOW = 40000000; // 40M 11 | 12 | export const DEFAULT_MAX_TOKENS = 4096; 13 | export const MAX_TOKENS = 16384; 14 | 15 | export const NUM_CTX_MESSAGES = 10; 16 | export const MAX_CTX_MESSAGES = 99; 17 | export const MIN_CTX_MESSAGES = 0; 18 | 19 | export const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB 20 | export const SUPPORTED_FILE_TYPES: { [key: string]: string } = { 21 | txt: 'text/plain', 22 | md: 'text/plain', 23 | csv: 'text/csv', 24 | epub: 'application/epub+zip', 25 | docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 26 | pdf: 'application/pdf', 27 | xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 28 | pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 29 | }; 30 | export const SUPPORTED_IMAGE_TYPES: { [key: string]: string } = { 31 | jpg: 'image/jpeg', 32 | jpeg: 'image/jpeg', 33 | png: 'image/png', 34 | }; 35 | -------------------------------------------------------------------------------- /src/d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | -------------------------------------------------------------------------------- /src/hooks/useECharts.ts: -------------------------------------------------------------------------------- 1 | import useAppearanceStore from 'stores/useAppearanceStore'; 2 | // @ts-ignore 3 | import * as echarts from 'echarts'; 4 | import { IChatMessage } from 'intellichat/types'; 5 | import { useMemo, useRef } from 'react'; 6 | import { useTranslation } from 'react-i18next'; 7 | 8 | const parseOption = (optStr: string) => { 9 | try { 10 | const match = optStr.match(/\{(.+)\}/s); 11 | if (!match) return {}; 12 | return new Function(`return {${match[1]}}`)(); 13 | } catch (error) { 14 | throw new Error('Invalid ECharts option format'); 15 | } 16 | }; 17 | 18 | export default function useECharts({ message }: { message: { id: string } }) { 19 | const { t } = useTranslation(); 20 | const theme = useAppearanceStore((state) => state.theme); 21 | const messageId = useMemo(() => message.id, [message]); 22 | const containersRef = useRef<{ [key: string]: echarts.EChartsType }>({}); 23 | 24 | const disposeECharts = () => { 25 | const chartInstances = Object.values(containersRef.current); 26 | chartInstances.forEach(({ cleanup }: { cleanup: Function }) => { 27 | cleanup(); 28 | }); 29 | containersRef.current = {}; 30 | }; 31 | 32 | const initECharts = (prefix: string, chartId: string) => { 33 | if (containersRef.current[`${prefix}-${chartId}`]) return; // already initialized 34 | const chartInstances = containersRef.current; 35 | const container = document.querySelector( 36 | `#${messageId} .echarts-container#${chartId}`, 37 | ) as HTMLDivElement; 38 | if (!container) return; 39 | const encodedConfig = container.getAttribute('data-echarts-config'); 40 | if (!encodedConfig) return; 41 | try { 42 | let config = decodeURIComponent(encodedConfig); 43 | const option = parseOption(config); 44 | const chart = echarts.init(container, theme); 45 | chart.setOption(option); 46 | const resizeHandler = () => chart.resize(); 47 | window.addEventListener('resize', resizeHandler); 48 | chartInstances[`${prefix}-${chartId}`] = { 49 | chartId, 50 | instance: chart, 51 | cleanup: () => { 52 | window.removeEventListener('resize', resizeHandler); 53 | chart.dispose(); 54 | }, 55 | }; 56 | } catch (error: any) { 57 | container.innerHTML = ` 58 |
59 | ${t('Message.Error.EChartsRenderFailed')} 60 |
61 | `; 62 | } 63 | }; 64 | 65 | return { 66 | disposeECharts, 67 | initECharts, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/hooks/useLazyEffect.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const useLazyEffect: typeof React.useEffect = (cb, dep) => { 4 | const initializeRef = React.useRef(false); 5 | 6 | React.useEffect((...args) => { 7 | if (initializeRef.current) { 8 | cb(...args); 9 | } else { 10 | initializeRef.current = true; 11 | } 12 | // eslint-disable-next-line react-hooks/exhaustive-deps 13 | }, dep); 14 | }; 15 | 16 | export default useLazyEffect; 17 | -------------------------------------------------------------------------------- /src/hooks/useMermaid.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import mermaid from 'mermaid/dist/mermaid'; 3 | import useAppearanceStore from 'stores/useAppearanceStore'; 4 | 5 | export default function useMermaid() { 6 | const theme = useAppearanceStore((state) => state.theme); 7 | return { 8 | renderMermaid() { 9 | mermaid.initialize({ theme: theme === 'dark' ? 'dark' : 'default' }); 10 | mermaid.init('div.mermaid'); 11 | }, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/useNav.ts: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | export default function useNav() { 4 | const navigate = useNavigate(); 5 | return (path: string) => { 6 | navigate(path); 7 | window.electron.ingestEvent([{ navigation: path }]); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/useOnlineStatus.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export default function useOnlineStatus() { 4 | const [isOnline, setIsOnline] = useState(true); 5 | useEffect(() => { 6 | function handleOnline() { 7 | setIsOnline(true); 8 | } 9 | function handleOffline() { 10 | setIsOnline(false); 11 | } 12 | window.addEventListener('online', handleOnline); 13 | window.addEventListener('offline', handleOffline); 14 | return () => { 15 | window.removeEventListener('online', handleOnline); 16 | window.removeEventListener('offline', handleOffline); 17 | }; 18 | }, []); 19 | return isOnline; 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/useToast.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useId, 3 | useToastController, 4 | Toast, 5 | ToastTitle, 6 | ToastBody, 7 | ToastIntent, 8 | } from '@fluentui/react-components'; 9 | 10 | import { 11 | Dismiss16Regular, 12 | Dismiss16Filled, 13 | bundleIcon, 14 | } from '@fluentui/react-icons'; 15 | 16 | const DismissIcon = bundleIcon(Dismiss16Filled, Dismiss16Regular); 17 | 18 | export default function useToast() { 19 | const { dispatchToast, dismissToast } = useToastController('toaster'); 20 | const toastId = useId('5ire'); 21 | const $notify = ({ 22 | title, 23 | message, 24 | intent, 25 | }: { 26 | title: string; 27 | message: string; 28 | intent: ToastIntent; 29 | }) => { 30 | dispatchToast( 31 | 32 | 33 |
34 | {title} 35 | 36 |
37 |
38 | 39 |
40 | {message} 41 |
42 |
43 |
, 44 | { toastId, intent, pauseOnHover: true, position: 'top-end' }, 45 | ); 46 | }; 47 | const dismiss = () => dismissToast(toastId); 48 | 49 | const notifyError = (message: string) => 50 | $notify({ title: 'Error', message, intent: 'error' }); 51 | const notifyWarning = (message: string) => 52 | $notify({ title: 'Warning', message, intent: 'warning' }); 53 | const notifyInfo = (message: string) => 54 | $notify({ title: 'Info', message, intent: 'info' }); 55 | const notifySuccess = (message: string) => 56 | $notify({ title: 'Success', message, intent: 'success' }); 57 | return { notifyError, notifyWarning, notifyInfo, notifySuccess }; 58 | } 59 | -------------------------------------------------------------------------------- /src/intellichat/readers/IChatReader.ts: -------------------------------------------------------------------------------- 1 | export interface ITool { 2 | id: string; 3 | name: string; 4 | args?: any; 5 | } 6 | 7 | export interface IReadResult { 8 | content: string; 9 | reasoning?: string; 10 | tool?: ITool | null; 11 | inputTokens?: number; 12 | outputTokens?: number; 13 | } 14 | export default interface IChatReader { 15 | read({ 16 | onError, 17 | onProgress, 18 | onToolCalls, 19 | }: { 20 | onError: (error: any) => void; 21 | onProgress: (chunk: string, reasoning?: string) => void; 22 | onToolCalls: (toolCalls: any) => void; 23 | }): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /src/intellichat/readers/OllamaChatReader.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { IChatResponseMessage } from 'intellichat/types'; 3 | import IChatReader from './IChatReader'; 4 | import OpenAIReader from './OpenAIReader'; 5 | 6 | const debug = Debug('5ire:intellichat:OllamaReader'); 7 | 8 | export default class OllamaReader extends OpenAIReader implements IChatReader { 9 | protected parseReply(chunk: string): IChatResponseMessage { 10 | const data = JSON.parse(chunk); 11 | if (data.done) { 12 | return { 13 | content: data.message.content, 14 | isEnd: true, 15 | inputTokens: data.prompt_eval_count, 16 | outputTokens: data.eval_count, 17 | }; 18 | } 19 | return { 20 | content: data.message.content, 21 | isEnd: false, 22 | toolCalls: data.message.tool_calls, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/intellichat/readers/OpenAIReader.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { IChatResponseMessage } from 'intellichat/types'; 3 | import BaseReader from './BaseReader'; 4 | import IChatReader, { ITool } from './IChatReader'; 5 | 6 | const debug = Debug('5ire:intellichat:OpenAIReader'); 7 | 8 | export default class OpenAIReader extends BaseReader implements IChatReader { 9 | protected parseReply(chunk: string): IChatResponseMessage { 10 | const data = JSON.parse(chunk); 11 | if (data.error) { 12 | throw new Error(data.error.message || data.error); 13 | } 14 | if (data.choices.length === 0) { 15 | return { 16 | content: '', 17 | reasoning: '', 18 | isEnd: false, 19 | toolCalls: [], 20 | }; 21 | } 22 | const choice = data.choices[0]; 23 | return { 24 | content: choice.delta.content || '', 25 | reasoning: choice.delta.reasoning_content || '', 26 | isEnd: false, 27 | toolCalls: choice.delta.tool_calls, 28 | }; 29 | } 30 | 31 | protected parseTools(respMsg: IChatResponseMessage): ITool | null { 32 | if (respMsg.toolCalls && respMsg.toolCalls.length > 0) { 33 | return { 34 | id: respMsg.toolCalls[0].id, 35 | name: respMsg.toolCalls[0].function.name, 36 | }; 37 | } 38 | return null; 39 | } 40 | 41 | protected parseToolArgs(respMsg: IChatResponseMessage): { 42 | index: number; 43 | args: string; 44 | } | null { 45 | try { 46 | if (respMsg.isEnd || !respMsg.toolCalls) { 47 | return null; 48 | } 49 | const toolCalls = respMsg.toolCalls[0]; 50 | return { 51 | index: toolCalls.index || 0, 52 | args: toolCalls.function?.arguments || '', 53 | }; 54 | } catch (err) { 55 | console.error('parseToolArgs', err); 56 | } 57 | return null; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/intellichat/services/AzureChatService.ts: -------------------------------------------------------------------------------- 1 | import { IChatContext, IChatRequestMessage } from 'intellichat/types'; 2 | import { urlJoin } from 'utils/util'; 3 | import OpenAIChatService from './OpenAIChatService'; 4 | import Azure from '../../providers/Azure'; 5 | import INextChatService from './INextCharService'; 6 | 7 | export default class AzureChatService 8 | extends OpenAIChatService 9 | implements INextChatService 10 | { 11 | constructor(name: string, chatContext: IChatContext) { 12 | super(name, chatContext); 13 | this.provider = Azure; 14 | } 15 | 16 | protected async makeRequest( 17 | messages: IChatRequestMessage[], 18 | msgId?: string, 19 | ): Promise { 20 | const provider = this.context.getProvider(); 21 | const model = this.context.getModel(); 22 | const deploymentId = model.extras?.deploymentId || model.name; 23 | const url = urlJoin( 24 | `/openai/deployments/${deploymentId}/chat/completions?api-version=${provider.apiVersion}`, 25 | provider.apiBase.trim(), 26 | ); 27 | const response = await fetch(url, { 28 | method: 'POST', 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | 'api-key': provider.apiKey, 32 | }, 33 | body: JSON.stringify(await this.makePayload(messages, msgId)), 34 | signal: this.abortController.signal, 35 | }); 36 | return response; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/intellichat/services/BaiduChatService.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { urlJoin } from 'utils/util'; 3 | import Baidu from '../../providers/Baidu'; 4 | import { IChatContext, IChatRequestMessage } from '../types'; 5 | import INextChatService from './INextCharService'; 6 | import OpenAIChatService from './OpenAIChatService'; 7 | 8 | const debug = Debug('5ire:intellichat:BaiduChatService'); 9 | 10 | export default class BaiduChatService 11 | extends OpenAIChatService 12 | implements INextChatService 13 | { 14 | constructor(name: string, context: IChatContext) { 15 | super(name, context); 16 | this.provider = Baidu; 17 | } 18 | 19 | protected async makeRequest( 20 | messages: IChatRequestMessage[], 21 | msgId?: string, 22 | ): Promise { 23 | const payload = await this.makePayload(messages, msgId); 24 | debug('About to make a request, payload:\r\n', payload); 25 | const provider = this.context.getProvider(); 26 | 27 | const apiKey = provider.apiKey.trim(); 28 | payload.model = (this.getModelName() as string).toLowerCase(); 29 | 30 | const url = urlJoin('/v2/chat/completions', provider.apiBase.trim()); 31 | const response = await fetch(url, { 32 | method: 'POST', 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | Authorization: `Bearer ${apiKey}`, 36 | }, 37 | body: JSON.stringify(payload), 38 | signal: this.abortController.signal, 39 | }); 40 | return response; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/intellichat/services/DeepSeekChatService.ts: -------------------------------------------------------------------------------- 1 | import { IChatContext, IChatRequestMessage } from 'intellichat/types'; 2 | import { urlJoin } from 'utils/util'; 3 | import OpenAIChatService from './OpenAIChatService'; 4 | import DeepSeek from '../../providers/DeepSeek'; 5 | import INextChatService from './INextCharService'; 6 | 7 | export default class DeepSeekChatService 8 | extends OpenAIChatService 9 | implements INextChatService 10 | { 11 | constructor(name:string, chatContext: IChatContext) { 12 | super(name, chatContext); 13 | this.provider = DeepSeek; 14 | } 15 | 16 | protected async makeRequest( 17 | messages: IChatRequestMessage[], 18 | msgId?: string, 19 | ): Promise { 20 | const provider = this.context.getProvider(); 21 | const url = urlJoin('/chat/completions', provider.apiBase.trim()); 22 | const response = await fetch(url, { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | Authorization: `Bearer ${provider.apiKey.trim()}`, 27 | }, 28 | body: JSON.stringify(await this.makePayload(messages, msgId)), 29 | signal: this.abortController.signal, 30 | }); 31 | return response; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/intellichat/services/DoubaoChatService.ts: -------------------------------------------------------------------------------- 1 | import { IChatContext, IChatRequestMessage } from 'intellichat/types'; 2 | import { urlJoin } from 'utils/util'; 3 | import OpenAIChatService from './OpenAIChatService'; 4 | import Doubao from '../../providers/Doubao'; 5 | import INextChatService from './INextCharService'; 6 | 7 | export default class DoubaoChatService 8 | extends OpenAIChatService 9 | implements INextChatService 10 | { 11 | constructor(name: string, chatContext: IChatContext) { 12 | super(name, chatContext); 13 | this.provider = Doubao; 14 | } 15 | 16 | protected async makeRequest( 17 | messages: IChatRequestMessage[], 18 | msgId?: string, 19 | ): Promise { 20 | const provider = this.context.getProvider(); 21 | const model = this.context.getModel(); 22 | const modelId = model.extras?.modelId || model.name; 23 | const payload = await this.makePayload(messages, msgId); 24 | payload.model = modelId; 25 | payload.stream = true; 26 | const url = urlJoin('/chat/completions', provider.apiBase.trim()); 27 | const response = await fetch(url, { 28 | method: 'POST', 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | Authorization: `Bearer ${provider.apiKey.trim()}`, 32 | }, 33 | body: JSON.stringify(payload), 34 | signal: this.abortController.signal, 35 | }); 36 | return response; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/intellichat/services/FireChatService.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { 3 | IChatContext, 4 | IChatRequestMessage, 5 | } from 'intellichat/types'; 6 | 7 | import Fire from 'providers/Fire'; 8 | import useAuthStore from 'stores/useAuthStore'; 9 | import { urlJoin } from 'utils/util'; 10 | import INextChatService from './INextCharService'; 11 | import OpenAIChatService from './OpenAIChatService'; 12 | 13 | const debug = Debug('5ire:intellichat:FireChatService'); 14 | 15 | export default class FireChatService 16 | extends OpenAIChatService 17 | implements INextChatService 18 | { 19 | constructor(name:string, context: IChatContext) { 20 | super(name, context); 21 | this.provider = Fire; 22 | } 23 | 24 | private getUserId() { 25 | const { session } = useAuthStore.getState(); 26 | return session?.user.id; 27 | } 28 | 29 | protected async makeRequest( 30 | messages: IChatRequestMessage[], 31 | msgId?: string 32 | ): Promise { 33 | const payload = await this.makePayload(messages, msgId); 34 | debug('About to make a request, payload:\r\n', payload); 35 | const provider = this.context.getProvider(); 36 | const key = this.getUserId(); 37 | if (!key) { 38 | throw new Error('User is not authenticated'); 39 | } 40 | const url = urlJoin(`/v1/chat/completions`, provider.apiBase.trim()); 41 | const response = await fetch(url, { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | Authorization: `Bearer ${key}`, 46 | }, 47 | body: JSON.stringify(payload), 48 | signal: this.abortController.signal, 49 | }); 50 | return response; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/intellichat/services/GrokChatService.ts: -------------------------------------------------------------------------------- 1 | import { IChatContext } from 'intellichat/types'; 2 | import OpenAIChatService from './OpenAIChatService'; 3 | import Grok from '../../providers/Grok'; 4 | import INextChatService from './INextCharService'; 5 | 6 | export default class GrokChatService 7 | extends OpenAIChatService 8 | implements INextChatService 9 | { 10 | constructor(name: string, chatContext: IChatContext) { 11 | super(name, chatContext); 12 | this.provider = Grok; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/intellichat/services/IChatService.ts: -------------------------------------------------------------------------------- 1 | import { IChatContext, IChatResponseMessage } from 'intellichat/types'; 2 | import { IServiceProvider } from 'providers/types'; 3 | 4 | export default interface IChatService { 5 | context: IChatContext; 6 | provider: IServiceProvider; 7 | apiSettings: { 8 | base: string; 9 | key: string; 10 | model: string; 11 | secret?: string; // baidu 12 | deploymentId?: string; // azure 13 | }; 14 | 15 | chat({ 16 | message, 17 | onMessage, 18 | onComplete, 19 | onError, 20 | }: { 21 | message: string; 22 | onMessage: (message: string) => void; 23 | onComplete: (result: IChatResponseMessage) => void; 24 | onError: (error: any, aborted: boolean) => void; 25 | }): void; 26 | abort(): void; 27 | isReady(): boolean; 28 | } 29 | -------------------------------------------------------------------------------- /src/intellichat/services/INextCharService.ts: -------------------------------------------------------------------------------- 1 | import { IChatContext, IChatRequestMessage } from 'intellichat/types'; 2 | import { IServiceProvider } from 'providers/types'; 3 | 4 | export default interface INextChatService { 5 | name: string; 6 | context: IChatContext; 7 | provider: IServiceProvider; 8 | chat(message: IChatRequestMessage[], msgId?: string): void; 9 | abort(): void; 10 | onComplete(callback: (result: any) => Promise): void; 11 | onToolCalls(callback: (toolName: string) => void): void; 12 | onReading(callback: (chunk: string, reasoning?: string) => void): void; 13 | onError(callback: (error: any, aborted: boolean) => void): void; 14 | } 15 | -------------------------------------------------------------------------------- /src/intellichat/services/LMStudioChatService.ts: -------------------------------------------------------------------------------- 1 | import { IChatContext } from 'intellichat/types'; 2 | import OpenAIChatService from './OpenAIChatService'; 3 | import LMStudio from '../../providers/LMStudio'; 4 | import INextChatService from './INextCharService'; 5 | 6 | export default class LMStudioChatService 7 | extends OpenAIChatService 8 | implements INextChatService 9 | { 10 | constructor(name:string, chatContext: IChatContext) { 11 | super(name, chatContext); 12 | this.provider = LMStudio; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/intellichat/services/MistralChatService.ts: -------------------------------------------------------------------------------- 1 | import { IChatContext } from 'intellichat/types'; 2 | import OpenAIChatService from './OpenAIChatService'; 3 | import Mistral from '../../providers/Mistral'; 4 | import INextChatService from './INextCharService'; 5 | 6 | export default class MistralChatService 7 | extends OpenAIChatService 8 | implements INextChatService 9 | { 10 | constructor(name:string, chatContext: IChatContext) { 11 | super(name, chatContext); 12 | this.provider = Mistral; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/intellichat/services/MoonshotChatService.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { IChatContext } from 'intellichat/types'; 3 | import Moonshot from 'providers/Moonshot'; 4 | import OpenAIChatService from './OpenAIChatService'; 5 | import INextChatService from './INextCharService'; 6 | 7 | // const debug = Debug('5ire:intellichat:MoonshotChatService'); 8 | 9 | export default class MoonshotChatService 10 | extends OpenAIChatService 11 | implements INextChatService 12 | { 13 | constructor(name:string, context: IChatContext) { 14 | super(name, context); 15 | this.provider = Moonshot; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/intellichat/services/index.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { IChatContext } from '../types'; 3 | import AnthropicChatService from './AnthropicChatService'; 4 | import AzureChatService from './AzureChatService'; 5 | import OllamaChatService from './OllamaChatService'; 6 | import LMStudioChatService from './LMStudioChatService'; 7 | import OpenAIChatService from './OpenAIChatService'; 8 | import GoogleChatService from './GoogleChatService'; 9 | import BaiduChatService from './BaiduChatService'; 10 | import MoonshotChatService from './MoonshotChatService'; 11 | import MistralChatService from './MistralChatService'; 12 | import FireChatService from './FireChatService'; 13 | import DoubaoChatService from './DoubaoChatService'; 14 | import GrokChatService from './GrokChatService'; 15 | import DeepSeekChatService from './DeepSeekChatService'; 16 | import INextChatService from './INextCharService'; 17 | 18 | const debug = Debug('5ire:intellichat:ChatService'); 19 | 20 | export default function createService(chatCtx: IChatContext): INextChatService { 21 | const provider = chatCtx.getProvider(); 22 | debug('CreateService', provider.name); 23 | switch (provider.name) { 24 | case 'Anthropic': 25 | return new AnthropicChatService(provider.name, chatCtx); 26 | case 'OpenAI': 27 | return new OpenAIChatService(provider.name, chatCtx); 28 | case 'Azure': 29 | return new AzureChatService(provider.name, chatCtx); 30 | case 'Google': 31 | return new GoogleChatService(provider.name, chatCtx); 32 | case 'Baidu': 33 | return new BaiduChatService(provider.name, chatCtx); 34 | case 'Mistral': 35 | return new MistralChatService(provider.name, chatCtx); 36 | case 'Moonshot': 37 | return new MoonshotChatService(provider.name, chatCtx); 38 | case 'Ollama': 39 | return new OllamaChatService(provider.name, chatCtx); 40 | case '5ire': 41 | return new FireChatService(provider.name, chatCtx); 42 | case 'Doubao': 43 | return new DoubaoChatService(provider.name, chatCtx); 44 | case 'Grok': 45 | return new GrokChatService(provider.name, chatCtx); 46 | case 'DeepSeek': 47 | return new DeepSeekChatService(provider.name, chatCtx); 48 | case 'LMStudio': 49 | return new LMStudioChatService(provider.name, chatCtx); 50 | default: 51 | return new OpenAIChatService(provider.name, chatCtx); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/intellichat/validators.ts: -------------------------------------------------------------------------------- 1 | import useProviderStore from 'stores/useProviderStore'; 2 | import { isNull, isNumber } from 'lodash'; 3 | import { isBlank } from 'utils/validators'; 4 | import { DEFAULT_MAX_TOKENS } from '../consts'; 5 | 6 | export function isValidMaxTokens( 7 | maxTokens: number | null | undefined, 8 | providerName: string, 9 | modelName: string, 10 | ): maxTokens is number | null { 11 | if (isNull(maxTokens)) return true; 12 | if (!isNumber(maxTokens)) return false; 13 | if (maxTokens <= 0) return false; 14 | 15 | const model = useProviderStore 16 | .getState() 17 | .getAvailableModel(providerName, modelName); 18 | return maxTokens <= (model.maxTokens || DEFAULT_MAX_TOKENS); 19 | } 20 | 21 | export function isValidTemperature( 22 | temperature: number | null | undefined, 23 | providerName: string, 24 | ): boolean { 25 | if (isBlank(providerName)) { 26 | return false; 27 | } 28 | if (!isNumber(temperature)) { 29 | return false; 30 | } 31 | const provider = useProviderStore 32 | .getState() 33 | .getAvailableProvider(providerName); 34 | const { min, max, interval } = provider.temperature; 35 | if (interval?.leftOpen ? temperature <= min : temperature < min) { 36 | return false; 37 | } 38 | if (interval?.rightOpen ? temperature >= max : temperature > max) { 39 | return false; 40 | } 41 | return true; 42 | } 43 | -------------------------------------------------------------------------------- /src/libs/markdownit-plugins/CodeCopy.ts: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | 3 | let clipboard: any = null; 4 | try { 5 | // Node js will throw an error 6 | this === window; 7 | 8 | const Clipboard = require('clipboard'); 9 | clipboard = new Clipboard('.markdown-it-code-copy'); 10 | } catch (_err) {} 11 | 12 | const defaultOptions = { 13 | iconStyle: 'font-size: 21px; opacity: 0.4;', 14 | iconClass: 'mdi mdi-content-copy', 15 | buttonStyle: 16 | 'position: absolute; top: 7.5px; right: 6px; cursor: pointer; outline: none;', 17 | buttonClass: '', 18 | element: '', 19 | }; 20 | 21 | function renderCode( 22 | origRule: (...args: [any, any]) => any, 23 | options: { 24 | buttonClass: any; 25 | buttonStyle: any; 26 | iconStyle: any; 27 | iconClass: any; 28 | element: any; 29 | }, 30 | ) { 31 | options = merge(defaultOptions, options); 32 | return (...args: [any, any]) => { 33 | const [tokens, idx] = args; 34 | const content = tokens[idx].content 35 | .replaceAll('"', '"') 36 | .replaceAll("'", '''); 37 | const origRendered = origRule(...args); 38 | if (content.length === 0) return origRendered; 39 | 40 | return ` 41 |
42 | ${origRendered} 43 | 46 |
47 | `; 48 | }; 49 | } 50 | 51 | export default function MarkdownItCodeCopy(md: any, options: any) { 52 | if (clipboard) { 53 | clipboard.off('success'); 54 | if (options.onSuccess) { 55 | clipboard.on('success', options.onSuccess); 56 | } 57 | clipboard.off('onError'); 58 | if (options.onError) { 59 | clipboard.on('error', options.onError); 60 | } 61 | } 62 | md.renderer.rules.code_block = renderCode( 63 | md.renderer.rules.code_block, 64 | options, 65 | ); 66 | md.renderer.rules.fence = renderCode(md.renderer.rules.fence, options); 67 | } 68 | -------------------------------------------------------------------------------- /src/libs/markdownit-plugins/markdownItEChartsPlugin.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import * as echarts from 'echarts'; 3 | //@ts-ignore 4 | import MarkdownIt from 'markdown-it'; 5 | //@ts-ignore 6 | import Token from 'markdown-it/lib/token.mjs'; 7 | 8 | export default function markdownItEChartsPlugin(md: MarkdownIt) { 9 | window.echarts = echarts; // 将 echarts 暴露到全局作用域中,以便在其他地方使用 10 | 11 | // 12 | const defaultFence = 13 | md.renderer.rules.fence || 14 | function ( 15 | tokens: Token[], 16 | idx: number, 17 | options: MarkdownIt.Options, 18 | env: any, 19 | self: MarkdownIt.Renderer, 20 | ) { 21 | return self.renderToken(tokens, idx, options); 22 | }; 23 | 24 | // 覆盖fence渲染规则 25 | interface EChartsToken extends Token { 26 | info: string; 27 | content: string; 28 | } 29 | 30 | md.renderer.rules.fence = ( 31 | tokens: Token[], 32 | idx: number, 33 | options: MarkdownIt.Options, 34 | env: any, 35 | self: MarkdownIt.Renderer, 36 | ): string => { 37 | const token = tokens[idx] as EChartsToken; 38 | const info = token.info.trim(); 39 | 40 | // only process code blocks with info 'echarts' 41 | if (info !== 'echarts') { 42 | return defaultFence(tokens, idx, options, env, self); 43 | } 44 | 45 | // generate a unique id for the chart container 46 | const chartId: string = 'echart-' + idx; 47 | const code: string = token.content.trim(); 48 | 49 | return ` 50 |
54 |
${code}
55 |
56 | `; 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/main/crypt.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'node:crypto'; 2 | 3 | const algorithm = 'aes-256-cbc'; // 选择加密算法 4 | const baseKey = process.env.CRYPTO_SECRET; // please change this key 5 | 6 | const makeKey = (key: string): string => { 7 | return crypto 8 | .createHash('sha256') 9 | .update(`${baseKey}.${key}`) 10 | .digest('base64') 11 | .substring(0, 32); 12 | }; 13 | 14 | export function encrypt( 15 | text: string, 16 | key: string, 17 | ): { iv: string; encrypted: string } { 18 | const iv = crypto.randomBytes(16); 19 | const cipher = crypto.createCipheriv(algorithm, makeKey(key), iv); 20 | const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); 21 | return { iv: iv.toString('hex'), encrypted: encrypted.toString('base64') }; 22 | } 23 | 24 | export function decrypt(encrypted: string, key: string, ivHex: string): string { 25 | const iv = Buffer.from(ivHex, 'hex'); 26 | const decipher = crypto.createDecipheriv(algorithm, makeKey(key), iv); 27 | let decrypted = decipher.update(encrypted, 'base64', 'utf8'); 28 | decrypted += decipher.final('utf8'); 29 | return decrypted; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/docloader.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import pdf from 'pdf-parse'; 3 | import officeParser from 'officeparser'; 4 | import * as logging from './logging'; 5 | 6 | abstract class BaseLoader { 7 | protected abstract read(filePath: string): Promise; 8 | 9 | async load(filePath: string): Promise { 10 | return await this.read(filePath); 11 | } 12 | } 13 | 14 | class TextDocumentLoader extends BaseLoader { 15 | async read(filePath: fs.PathLike): Promise { 16 | return await fs.promises.readFile(filePath, 'utf-8'); 17 | } 18 | } 19 | 20 | class OfficeLoader extends BaseLoader { 21 | constructor() { 22 | super(); 23 | } 24 | 25 | async read(filePath: string): Promise { 26 | return new Promise((resolve, reject) => { 27 | officeParser.parseOffice(filePath, function (text: string, error: any) { 28 | if (error) { 29 | reject(error); 30 | } else { 31 | resolve(text); 32 | } 33 | }); 34 | }); 35 | } 36 | } 37 | 38 | class PdfLoader extends BaseLoader { 39 | async read(filePath: fs.PathLike): Promise { 40 | const dataBuffer = fs.readFileSync(filePath); 41 | const data = await pdf(dataBuffer); 42 | return data.text; 43 | } 44 | } 45 | 46 | export async function loadDocument( 47 | filePath: string, 48 | fileType: string, 49 | ): Promise { 50 | logging.info(`load file from ${filePath} on ${process.platform}`); 51 | let Loader: new () => BaseLoader; 52 | switch (fileType) { 53 | case 'txt': 54 | Loader = TextDocumentLoader; 55 | break; 56 | case 'md': 57 | Loader = TextDocumentLoader; 58 | break; 59 | case 'csv': 60 | Loader = TextDocumentLoader; 61 | break; 62 | case 'pdf': 63 | Loader = PdfLoader; 64 | break; 65 | case 'docx': 66 | Loader = OfficeLoader; 67 | break; 68 | case 'pptx': 69 | Loader = OfficeLoader; 70 | break; 71 | case 'xlsx': 72 | Loader = OfficeLoader; 73 | break; 74 | default: 75 | throw new Error(`Miss Loader for: ${fileType}`); 76 | } 77 | const loader = new Loader(); 78 | let result = await loader.load(filePath); 79 | result = result.replace(/ +/g, ' '); 80 | const paragraphs = result 81 | .split(/\r?\n\r?\n/) 82 | .map((i) => i.replace(/\s+/g, ' ')) 83 | .filter((i) => i.trim() !== ''); 84 | return paragraphs.join('\r\n\r\n'); 85 | } 86 | -------------------------------------------------------------------------------- /src/main/downloader.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import log from 'electron-log'; 4 | import { app } from 'electron'; 5 | 6 | export default class Downloader { 7 | private win: any; 8 | 9 | private downloads: { [key: string]: any } = {}; 10 | 11 | private onFailed: Function | undefined; 12 | 13 | constructor( 14 | win: any, 15 | { onStart, onCompleted, onFailed, onProgress } = { 16 | onStart: (fileName: string) => {}, 17 | onCompleted: (fileName: string, savePath: string) => {}, 18 | onFailed: (fileName: string, savePath: string, state: string) => {}, 19 | onProgress: (fileName: string, progress: number) => {}, 20 | }, 21 | ) { 22 | this.win = win; 23 | this.onFailed = onFailed; 24 | 25 | this.win.webContents.session.on('will-download', (evt: any, item: any) => { 26 | const fileName = item.getFilename(); 27 | const savePath = path.join( 28 | app.getPath('userData'), 29 | 'downloads', 30 | item.getFilename(), 31 | ); 32 | item.setSavePath(savePath); 33 | 34 | const download = this.downloads[item.getFilename()]; 35 | if (download?.cancelled) { 36 | delete this.downloads[item.getFilename()]; 37 | item.cancel(); 38 | try { 39 | fs.unlinkSync(savePath); 40 | } catch (e) { 41 | log.error('error deleting file', savePath, e); 42 | } 43 | onFailed && onFailed(fileName, savePath, 'cancelled'); 44 | return; 45 | } 46 | this.downloads[fileName] = item; 47 | onStart && onStart(fileName); 48 | 49 | item.on('updated', (_: any, state: string) => { 50 | if (state === 'progressing') { 51 | const progress = item.getReceivedBytes() / item.getTotalBytes(); 52 | onProgress && onProgress(fileName, progress); 53 | } 54 | }); 55 | 56 | item.once('done', (_: Electron.Event, state: string) => { 57 | log.debug(`Download ${state}`, fileName); 58 | if (state === 'completed') { 59 | onCompleted && onCompleted(fileName, savePath); 60 | } else { 61 | fs.unlink(savePath, (err) => { 62 | if (err) { 63 | log.warn('error deleting file', savePath, err); 64 | } 65 | }); 66 | onFailed && onFailed(fileName, savePath, state); 67 | } 68 | delete this.downloads[fileName]; 69 | }); 70 | }); 71 | } 72 | 73 | download(fileName: string, url: string) { 74 | this.downloads[fileName] = { pending: true }; 75 | this.win.webContents.session.downloadURL(url); 76 | } 77 | 78 | cancel(fileName: string) { 79 | let item = this.downloads[fileName]; 80 | if (!item) { 81 | this.downloads[fileName] = item = { pending: true }; 82 | } 83 | if (!item.pending) { 84 | log.debug(`Cancelling download ${fileName}`); 85 | item.cancel(); 86 | delete this.downloads[fileName]; 87 | this.onFailed && this.onFailed(fileName, 'cancelled'); 88 | fs.unlink(item.getSavePath(), (error) => { 89 | if (error) { 90 | log.warn('error deleting file', item.getSavePath(), error); 91 | } 92 | }); 93 | } else { 94 | item.cancelled = true; 95 | log.warn('Download not started yet, set to be cancelled on start'); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/logging.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/electron/main'; 2 | import log from 'electron-log'; 3 | 4 | export function init() { 5 | if (process.env.SENTRY_DSN && process.env.NODE_ENV !== 'development') { 6 | Sentry.init({ 7 | dsn: process.env.SENTRY_DSN, 8 | }); 9 | } 10 | } 11 | 12 | export function captureException(error: Error | string) { 13 | log.error(error); 14 | if (process.env.SENTRY_DSN && process.env.NODE_ENV !== 'development') { 15 | Sentry.captureException(error); 16 | } 17 | } 18 | 19 | export function captureWarning(warning: any) { 20 | log.warn(warning); 21 | if (process.env.SENTRY_DSN && process.env.NODE_ENV !== 'development') { 22 | Sentry.captureMessage(warning, 'warning'); 23 | } 24 | } 25 | 26 | export function debug(...messages: any[]) { 27 | log.debug(messages); 28 | } 29 | 30 | export function info(...messages: any[]) { 31 | log.info(...messages); 32 | } 33 | 34 | export function warn(...messages: any[]) { 35 | log.warn(...messages); 36 | } 37 | 38 | export function error(...messages: any[]) { 39 | log.error(...messages); 40 | } 41 | -------------------------------------------------------------------------------- /src/providers/Baidu.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | const chatModels = [ 4 | { 5 | id: 'ERNIE-4.0-8K', 6 | name: 'ERNIE-4.0-8K', 7 | contextWindow: 5120, 8 | maxTokens: 2048, 9 | inputPrice: 0.03, 10 | outputPrice: 0.09, 11 | description: `百度自研的旗舰级超大规模⼤语⾔模型,相较ERNIE 3.5实现了模型能力全面升级,广泛适用于各领域复杂任务场景;支持自动对接百度搜索插件,保障问答信息时效。 百度文心系列中效果最强大的⼤语⾔模型,理解、生成、逻辑、记忆能力达到业界顶尖水平。`, 12 | isDefault: true, 13 | }, 14 | { 15 | id: 'ERNIE-4.0-8K-Preview', 16 | name: 'ERNIE-4.0-8K-Preview', 17 | contextWindow: 5120, 18 | maxTokens: 2048, 19 | inputPrice: 0.03, 20 | outputPrice: 0.09, 21 | description: `百度自研的旗舰级超大规模⼤语⾔模型,相较ERNIE 3.5实现了模型能力全面升级,广泛适用于各领域复杂任务场景;支持自动对接百度搜索插件,保障问答信息时效。`, 22 | }, 23 | { 24 | id: 'ERNIE-4.0-8K-Latest', 25 | name: 'ERNIE-4.0-8K-Latest', 26 | contextWindow: 5120, 27 | maxTokens: 2048, 28 | inputPrice: 0.03, 29 | outputPrice: 0.09, 30 | description: `ERNIE-4.0-8K-Latest相比ERNIE-4.0-8K能力全面提升,其中角色扮演能力和指令遵循能力提升较大;相较ERNIE 3.5实现了模型能力全面升级,广泛适用于各领域复杂任务场景;支持自动对接百度搜索插件,保障问答信息时效,支持5K tokens输入+2K tokens输出。`, 31 | }, 32 | { 33 | id: 'ERNIE-4.0-Turbo-8K', 34 | name: 'ERNIE-4.0-Turbo-8K', 35 | contextWindow: 5120, 36 | maxTokens: 2048, 37 | inputPrice: 0.03, 38 | outputPrice: 0.09, 39 | description: `ERNIE 4.0 Turbo是百度自研的旗舰级超大规模⼤语⾔模型,综合效果表现出色,广泛适用于各领域复杂任务场景;支持自动对接百度搜索插件,保障问答信息时效。相较于ERNIE 4.0在性能表现上更优秀`, 40 | }, 41 | { 42 | id: 'ERNIE-4.0-Turbo-8K-Preview', 43 | name: 'ERNIE-4.0-Turbo-8K-Preview', 44 | contextWindow: 5120, 45 | maxTokens: 2048, 46 | inputPrice: 0.03, 47 | outputPrice: 0.09, 48 | description: `ERNIE 4.0 Turbo是百度自研的旗舰级超大规模⼤语⾔模型,综合效果表现出色,广泛适用于各领域复杂任务场景;支持自动对接百度搜索插件,保障问答信息时效。相较于ERNIE 4.0在性能表现上更优秀`, 49 | }, 50 | { 51 | id: 'ERNIE-4.0-Turbo-8K-Latest', 52 | name: 'ERNIE-4.0-Turbo-8K-Latest', 53 | contextWindow: 5120, 54 | maxTokens: 2048, 55 | inputPrice: 0.03, 56 | outputPrice: 0.09, 57 | description: `ERNIE 4.0 Turbo是百度自研的旗舰级超大规模⼤语⾔模型,综合效果表现出色,广泛适用于各领域复杂任务场景;支持自动对接百度搜索插件,保障问答信息时效。相较于ERNIE 4.0在性能表现上更优秀`, 58 | }, 59 | { 60 | id: 'ERNIE-3.5-8K', 61 | name: 'ERNIE-3.5-8K', 62 | contextWindow: 124000, 63 | maxTokens: 2048, 64 | inputPrice: 0.0008, 65 | outputPrice: 0.002, 66 | description: `百度自研的旗舰级大规模⼤语⾔模型,覆盖海量中英文语料,具有强大的通用能力,可满足绝大部分对话问答、创作生成、插件应用场景要求;支持自动对接百度搜索插件,保障问答信息时效。`, 67 | }, 68 | ]; 69 | 70 | export default { 71 | name: 'Baidu', 72 | apiBase: 'https://qianfan.baidubce.com', 73 | currency: 'CNY', 74 | options: { 75 | apiBaseCustomizable: false, 76 | apiKeyCustomizable: true, 77 | }, 78 | description: 79 | '[API key] 的获取参考:https://cloud.baidu.com/doc/qianfan-api/s/ym9chdsy5', 80 | chat: { 81 | apiSchema: ['base', 'key'], 82 | docs: { 83 | apiKey: '用户账号->安全认证->[API Key]', 84 | }, 85 | presencePenalty: { min: 1, max: 2, default: 1 }, // penalty_score 86 | topP: { min: 0, max: 1, default: 0.8 }, // (0, 1] 87 | temperature: { 88 | min: 0, 89 | max: 1, 90 | default: 0.95, 91 | interval: { 92 | leftOpen: true, 93 | rightOpen: false, 94 | }, 95 | }, // (0, 1] 96 | options: { 97 | modelCustomizable: true, 98 | }, 99 | models: chatModels, 100 | }, 101 | } as IServiceProvider; 102 | -------------------------------------------------------------------------------- /src/providers/DeepSeek.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | const chatModels = [ 4 | { 5 | id: 'deepseek-chat', 6 | name: 'deepseek-chat', 7 | contextWindow: 65536, 8 | maxTokens: 8192, 9 | defaultMaxTokens: 8000, 10 | inputPrice: 0.0006, 11 | outputPrice: 0.002, 12 | isDefault: true, 13 | description: `60 tokens/second, Enhanced capabilities,API compatibility intact`, 14 | capabilities: { 15 | tools: { 16 | enabled: true, 17 | }, 18 | }, 19 | }, 20 | { 21 | id: 'deepseek-reasoner', 22 | name: 'deepseek-reasoner', 23 | contextWindow: 65536, 24 | maxTokens: 8192, 25 | defaultMaxTokens: 8000, 26 | inputPrice: 0.003, 27 | outputPrice: 0.016, 28 | isDefault: true, 29 | description: `Performance on par with OpenAI-o1`, 30 | capabilities: { 31 | tools: null, 32 | }, 33 | }, 34 | ]; 35 | 36 | export default { 37 | name: 'DeepSeek', 38 | apiBase: 'https://api.deepseek.com', 39 | currency: 'CNY', 40 | options: { 41 | apiBaseCustomizable: true, 42 | apiKeyCustomizable: true, 43 | }, 44 | chat: { 45 | apiSchema: ['base', 'key'], 46 | presencePenalty: { min: -2, max: 2, default: 0 }, 47 | topP: { min: 0, max: 1, default: 1 }, 48 | temperature: { min: 0, max: 2, default: 1 }, 49 | options: { 50 | modelCustomizable: true, 51 | }, 52 | models: chatModels, 53 | }, 54 | } as IServiceProvider; 55 | -------------------------------------------------------------------------------- /src/providers/LMStudio.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | export default { 4 | name: 'LMStudio', 5 | apiBase: 'http://127.0.0.1:1234/v1', 6 | currency: 'USD', 7 | options: { 8 | apiBaseCustomizable: true, 9 | modelsEndpoint: '/models', 10 | }, 11 | chat: { 12 | apiSchema: ['base', 'model'], 13 | docs: { 14 | temperature: 15 | 'Higher values will make the output more creative and unpredictable, while lower values will make it more precise.', 16 | presencePenalty: 17 | "Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", 18 | topP: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with topP probability mass.', 19 | }, 20 | placeholders: { 21 | base: ' http://127.0.0.1:1234/v1', 22 | }, 23 | presencePenalty: { min: -2, max: 2, default: 0 }, 24 | topP: { min: 0, max: 1, default: 1 }, 25 | temperature: { min: 0, max: 1, default: 0.9 }, 26 | 27 | options: { 28 | modelCustomizable: true, 29 | }, 30 | models: [], 31 | }, 32 | embedding: { 33 | apiSchema: ['base'], 34 | placeholders: { 35 | base: ' http://127.0.0.1:1234/v1', 36 | }, 37 | options: { 38 | modelCustomizable: true, 39 | }, 40 | models: [], 41 | }, 42 | } as IServiceProvider; 43 | -------------------------------------------------------------------------------- /src/providers/Moonshot.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | const chatModels = [ 4 | { 5 | id: 'moonshot-v1-8k', 6 | name: 'moonshot-v1-8k', 7 | contextWindow: 8192, 8 | maxTokens: 1024, 9 | inputPrice: 0.012, 10 | outputPrice: 0.012, 11 | isDefault: true, 12 | capabilities: { 13 | tools: { 14 | enabled: true, 15 | }, 16 | }, 17 | }, 18 | { 19 | id: 'moonshot-v1-32k', 20 | name: 'moonshot-v1-32k', 21 | contextWindow: 32768, 22 | maxTokens: 1024, 23 | inputPrice: 0.024, 24 | outputPrice: 0.024, 25 | capabilities: { 26 | tools: { 27 | enabled: true, 28 | }, 29 | }, 30 | }, 31 | { 32 | id: 'moonshot-v1-128k', 33 | name: 'moonshot-v1-128k', 34 | contextWindow: 128000, 35 | maxTokens: 1024, 36 | inputPrice: 0.06, 37 | outputPrice: 0.06, 38 | capabilities: { 39 | tools: { 40 | enabled: true, 41 | }, 42 | }, 43 | }, 44 | ]; 45 | 46 | export default { 47 | name: 'Moonshot', 48 | apiBase: 'https://api.moonshot.cn/v1', 49 | currency: 'CNY', 50 | options: { 51 | apiBaseCustomizable: true, 52 | apiKeyCustomizable: true, 53 | }, 54 | chat: { 55 | apiSchema: ['base', 'key'], 56 | presencePenalty: { min: -2, max: 2, default: 0 }, 57 | topP: { min: 0, max: 1, default: 1 }, 58 | temperature: { min: 0, max: 1, default: 0.3 }, 59 | options: { 60 | modelCustomizable: true, 61 | }, 62 | models: chatModels, 63 | }, 64 | } as IServiceProvider; 65 | -------------------------------------------------------------------------------- /src/providers/Ollama.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | export default { 4 | name: 'Ollama', 5 | apiBase: 'http://127.0.0.1:11434', 6 | currency: 'USD', 7 | options: { 8 | apiBaseCustomizable: true, 9 | modelsEndpoint: '/api/tags', 10 | }, 11 | chat: { 12 | apiSchema: ['base'], 13 | docs: { 14 | temperature: 15 | 'Higher values will make the output more creative and unpredictable, while lower values will make it more precise.', 16 | presencePenalty: 17 | "Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", 18 | topP: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with topP probability mass.', 19 | }, 20 | placeholders: { 21 | base: ' http://127.0.0.1:11434', 22 | }, 23 | presencePenalty: { min: -2, max: 2, default: 0 }, 24 | topP: { min: 0, max: 1, default: 1 }, 25 | temperature: { min: 0, max: 1, default: 0.9 }, 26 | 27 | options: { 28 | modelCustomizable: true, 29 | }, 30 | models: [], 31 | }, 32 | embedding: { 33 | apiSchema: ['base'], 34 | placeholders: { 35 | base: ' http://127.0.0.1:11434', 36 | }, 37 | options: { 38 | modelCustomizable: true, 39 | }, 40 | models: [], 41 | }, 42 | } as IServiceProvider; 43 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { ProviderType, IServiceProvider } from './types'; 2 | import Azure from './Azure'; 3 | import Baidu from './Baidu'; 4 | import OpenAI from './OpenAI'; 5 | import Google from './Google'; 6 | import Moonshot from './Moonshot'; 7 | import Anthropic from './Anthropic'; 8 | import Fire from './Fire'; 9 | import Ollama from './Ollama'; 10 | import LMStudio from './LMStudio'; 11 | import Doubao from './Doubao'; 12 | import Grok from './Grok'; 13 | import DeepSeek from './DeepSeek'; 14 | import Mistral from './Mistral'; 15 | 16 | export const providers: { [key: string]: IServiceProvider } = { 17 | OpenAI, 18 | Anthropic, 19 | Azure, 20 | Google, 21 | Grok, 22 | Baidu, 23 | Mistral, 24 | Moonshot, 25 | Ollama, 26 | Doubao, 27 | DeepSeek, 28 | LMStudio, 29 | '5ire': Fire, 30 | }; 31 | 32 | // TODO: about to remove 33 | export function getProvider(providerName: ProviderType): IServiceProvider { 34 | return providers[providerName]; 35 | } 36 | 37 | export function getBuiltInProviders(): IServiceProvider[] { 38 | return Object.values(providers); 39 | } 40 | 41 | export function getChatAPISchema(providerName: string): string[] { 42 | const provider = providers[providerName]; 43 | if (!provider) { 44 | return OpenAI.chat.apiSchema; // Fallback to OpenAI if provider not found 45 | } 46 | return provider.chat.apiSchema; 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import useAuthStore from 'stores/useAuthStore'; 3 | import { useEffect } from 'react'; 4 | import useToast from 'hooks/useToast'; 5 | import { useTranslation } from 'react-i18next'; 6 | import useKnowledgeStore from 'stores/useKnowledgeStore'; 7 | import useMCPStore from 'stores/useMCPStore'; 8 | import Mousetrap from 'mousetrap'; 9 | import FluentApp from './components/FluentApp'; 10 | import * as logging from './logging'; 11 | 12 | import './App.scss'; 13 | import './fluentui.scss'; 14 | 15 | if (window.envVars.NODE_ENV === 'development') { 16 | Debug.enable('5ire:*'); 17 | } 18 | 19 | const debug = Debug('5ire:App'); 20 | 21 | logging.init(); 22 | 23 | export default function App() { 24 | const loadAuthData = useAuthStore((state) => state.load); 25 | const setSession = useAuthStore((state) => state.setSession); 26 | const { loadConfig, updateLoadingState } = useMCPStore(); 27 | const { onAuthStateChange } = useAuthStore(); 28 | const { notifyError } = useToast(); 29 | const { t } = useTranslation(); 30 | const { createFile } = useKnowledgeStore(); 31 | 32 | useEffect(() => { 33 | loadAuthData(); 34 | Mousetrap.prototype.stopCallback = () => { 35 | return false; 36 | }; 37 | const subscription = onAuthStateChange(); 38 | window.electron.mcp.init(); 39 | window.electron.ipcRenderer.on( 40 | 'mcp-server-loaded', 41 | async (serverNames: any) => { 42 | debug('🚩 MCP Server Loaded:', serverNames); 43 | loadConfig(true); 44 | updateLoadingState(false); 45 | }, 46 | ); 47 | 48 | window.electron.ipcRenderer.on('sign-in', async (authData: any) => { 49 | if (authData.accessToken && authData.refreshToken) { 50 | const { error } = await setSession(authData); 51 | if (error) { 52 | notifyError(error.message); 53 | } 54 | } else { 55 | debug('🚩 Invalid Auth Data:', authData); 56 | notifyError(t('Auth.Notification.LoginCallbackFailed')); 57 | } 58 | }); 59 | 60 | /** 61 | * 当知识库导入任务完成时触发 62 | * 放這是为了避免组件卸载后无法接收到事件 63 | */ 64 | window.electron.ipcRenderer.on( 65 | 'knowledge-import-success', 66 | (data: unknown) => { 67 | const { collectionId, file, numOfChunks } = data as any; 68 | createFile({ 69 | id: file.id, 70 | collectionId, 71 | name: file.name, 72 | size: file.size, 73 | numOfChunks, 74 | }); 75 | }, 76 | ); 77 | 78 | return () => { 79 | window.electron.ipcRenderer.unsubscribeAll('mcp-server-loaded'); 80 | window.electron.ipcRenderer.unsubscribeAll('sign-in'); 81 | window.electron.ipcRenderer.unsubscribeAll('knowledge-import-success'); 82 | subscription.unsubscribe(); 83 | }; 84 | }, [loadAuthData, onAuthStateChange]); 85 | 86 | return ; 87 | } 88 | -------------------------------------------------------------------------------- /src/renderer/apps/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, lazy, Suspense } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { IAppConfig } from './types'; 4 | import apps from './index'; 5 | 6 | const APPS: Record< 7 | string, 8 | React.LazyExoticComponent<() => React.ReactElement> 9 | > = apps.reduce( 10 | ( 11 | acc: Record React.ReactElement>>, 12 | app: IAppConfig, 13 | ) => { 14 | acc[app.key] = lazy(() => import(`./${app.key}/App`)); 15 | return acc; 16 | }, 17 | {}, 18 | ); 19 | 20 | const NotFound = lazy(() => import('./NotFound')); 21 | 22 | export default function Loader() { 23 | const { key } = useParams(); 24 | const [app, setApp] = useState(); 25 | 26 | useEffect(() => { 27 | const importApp = async () => { 28 | const config: IAppConfig | undefined = apps.find( 29 | (app: IAppConfig) => app.key === key, 30 | ); 31 | if (!config || !config.isEnabled) { 32 | setApp(); 33 | return; 34 | } 35 | const App = APPS[config.key] || NotFound; 36 | setApp(); 37 | }; 38 | importApp(); 39 | }, [key]); 40 | 41 | return ( 42 |
43 | Loading...
}>{app} 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/apps/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | import Empty from '../components/Empty'; 3 | 4 | export default function NotFound() { 5 | const { t } = useTranslation(); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/apps/index.ts: -------------------------------------------------------------------------------- 1 | // unused component 2 | import leonardo from './leonardo'; 3 | import sora from './sora'; 4 | 5 | export default [leonardo, sora]; 6 | -------------------------------------------------------------------------------- /src/renderer/apps/leonardo/App.tsx: -------------------------------------------------------------------------------- 1 | // unused component 2 | export default function Leonardo() { 3 | return
Leonardo
; 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/apps/leonardo/app-leonardo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/renderer/apps/leonardo/app-leonardo.png -------------------------------------------------------------------------------- /src/renderer/apps/leonardo/index.tsx: -------------------------------------------------------------------------------- 1 | import icon from './app-leonardo.png'; 2 | 3 | export default { 4 | name: 'Leonardo', 5 | description: 'Leonardo generates images from natural language descriptions', 6 | isEnabled: false, 7 | key: 'leonardo', 8 | icon, 9 | }; 10 | -------------------------------------------------------------------------------- /src/renderer/apps/sora/App.tsx: -------------------------------------------------------------------------------- 1 | // unused component 2 | export default function Leonardo() { 3 | return
Leonardo
; 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/apps/sora/app-sora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/src/renderer/apps/sora/app-sora.png -------------------------------------------------------------------------------- /src/renderer/apps/sora/index.tsx: -------------------------------------------------------------------------------- 1 | import icon from './app-sora.png'; 2 | 3 | export default { 4 | name: 'Sora', 5 | description: 'Sora generates videos from natural language descriptions', 6 | isEnabled: false, 7 | key: 'sora', 8 | icon, 9 | }; 10 | -------------------------------------------------------------------------------- /src/renderer/apps/types.ts: -------------------------------------------------------------------------------- 1 | export interface IAppConfig { 2 | name: string; 3 | description: string; 4 | isEnabled: boolean; 5 | isPremium?: boolean; 6 | key: string; 7 | icon: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/components/AlertDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogSurface, 4 | DialogBody, 5 | DialogTitle, 6 | DialogContent, 7 | DialogActions, 8 | DialogTrigger, 9 | Button, 10 | } from '@fluentui/react-components'; 11 | import DOMPurify from 'dompurify'; 12 | import { 13 | CheckmarkCircle24Filled, 14 | Warning24Filled, 15 | ErrorCircle24Filled, 16 | Info24Filled, 17 | } from '@fluentui/react-icons'; 18 | import { useCallback } from 'react'; 19 | import { useTranslation } from 'react-i18next'; 20 | import useAppearanceStore from 'stores/useAppearanceStore'; 21 | 22 | export default function AlertDialog(args: { 23 | type: 'success' | 'warning' | 'error' | 'info'; 24 | open: boolean; 25 | setOpen: (open: boolean) => void; 26 | title?: string; 27 | message: string; 28 | onConfirm?: () => void; 29 | }) { 30 | const { t } = useTranslation(); 31 | const { type, open, setOpen, title, message, onConfirm } = args; 32 | const { getPalette } = useAppearanceStore.getState(); 33 | const renderIcon = useCallback(() => { 34 | switch (type) { 35 | case 'success': 36 | return ( 37 | 41 | ); 42 | case 'warning': 43 | return ( 44 | 48 | ); 49 | case 'error': 50 | return ( 51 | 55 | ); 56 | case 'info': 57 | return ( 58 | 59 | ); 60 | default: 61 | return null; 62 | } 63 | }, [type]); 64 | 65 | return ( 66 | setOpen(data.open)} 70 | > 71 | 72 | 73 | 74 | {renderIcon()} 75 | {title} 76 | 77 | 78 |
79 | 80 | 81 | 82 | 91 | 92 | 93 | 94 | 95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/renderer/components/Assets.tsx: -------------------------------------------------------------------------------- 1 | // import DarkDesign from 'assets/images/design.dark.png'; //704 2 | // import LightDesign from 'assets/images/design.light.png'; 3 | // import DarkConstruction from 'assets/images/construction.dark.png'; //744 4 | // import LightConstruction from 'assets/images/construction.light.png'; 5 | // import DarkDoor from 'assets/images/door.dark.png'; //935 6 | // import LightDoor from 'assets/images/door.light.png'; 7 | // import DarkReading from 'assets/images/reading.dark.png'; //1105 8 | // import LightReading from 'assets/images/reading.light.png' 9 | // import DarkUsage from 'assets/images/usage.dark.png'; //1181 10 | // import LightUsage from 'assets/images/usage.light.png'; 11 | // import DarkHint from 'assets/images/hint.dark.png'; //1387 12 | // import LightHint from 'assets/images/hint.light.png'; 13 | 14 | export function getImage(name: string, theme?: 'light' | 'dark') { 15 | if (theme) { 16 | return require(`assets/images/${name}.${theme}.png`); 17 | } 18 | return require(`assets/images/${name}`); 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/components/ChatFolders.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Accordion, 3 | AccordionToggleEventHandler, 4 | } from '@fluentui/react-components'; 5 | import { IChat } from 'intellichat/types'; 6 | import { useCallback, useEffect, useMemo, useRef } from 'react'; 7 | import useChatStore from 'stores/useChatStore'; 8 | import { TEMP_CHAT_ID } from 'consts'; 9 | import ChatFolder from './ChatFolder'; 10 | 11 | export default function ChatFolders({ 12 | chats, 13 | collapsed, 14 | }: { 15 | chats: IChat[]; 16 | collapsed: boolean; 17 | }) { 18 | const chat = useChatStore((state) => state.chat); 19 | const folder = useChatStore((state) => state.folder); 20 | const folders = useChatStore((state) => state.folders); 21 | const openFolders = useChatStore((state) => state.openFolders); 22 | const { initChat, selectFolder, getCurFolderSettings, setOpenFolders } = 23 | useChatStore(); 24 | const clickCountRef = useRef(0); 25 | 26 | const chatsGroupByFolder = useMemo(() => { 27 | const groups = chats.reduce( 28 | function (acc, cht) { 29 | const folderId = cht.folderId as string; 30 | if (!acc[folderId]) { 31 | acc[folderId] = []; 32 | } 33 | acc[folderId].push(cht); 34 | return acc; 35 | }, 36 | {} as Record, 37 | ); 38 | return groups; 39 | }, [chats]); 40 | 41 | const handleToggle = useCallback( 42 | function (_, data) { 43 | clickCountRef.current += 1; 44 | if (clickCountRef.current % 2 === 0) { 45 | clickCountRef.current = 0; 46 | return; 47 | } 48 | 49 | const timer = setTimeout(() => { 50 | if (clickCountRef.current % 2 !== 0) { 51 | selectFolder(data.value as string); 52 | setOpenFolders(data.openItems as string[]); 53 | } 54 | clickCountRef.current = 0; 55 | }, 200); 56 | 57 | return () => { 58 | clearTimeout(timer); 59 | }; 60 | }, 61 | [chat.id], 62 | ); 63 | 64 | useEffect(() => { 65 | if (folder && chat.id === TEMP_CHAT_ID) { 66 | initChat(getCurFolderSettings()); 67 | } 68 | }, [folder, chat.id]); 69 | 70 | return ( 71 | 77 | {Object.keys(folders) 78 | .sort() 79 | .map((folderId) => { 80 | const fld = folders[folderId]; 81 | const chatsInFolder = chatsGroupByFolder[folderId]; 82 | return ( 83 | 89 | ); 90 | })} 91 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/renderer/components/ChatIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from '@fluentui/react-components'; 2 | import { Chat20Filled, Chat20Regular } from '@fluentui/react-icons'; 3 | import { IChat } from 'intellichat/types'; 4 | import useChatStore from 'stores/useChatStore'; 5 | import Spinner from './Spinner'; 6 | 7 | export default function ChatIcon({ 8 | chat, 9 | isActive, 10 | }: { 11 | chat: IChat; 12 | isActive: boolean; 13 | }) { 14 | const chatStates = useChatStore((state) => state.states); 15 | 16 | const renderChatIcon = () => { 17 | if (chatStates[chat.id]?.loading) { 18 | return ; 19 | } 20 | if (isActive) { 21 | return ; 22 | } 23 | return ; 24 | }; 25 | 26 | return ( 27 | 33 | {renderChatIcon()} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/components/ConfirmDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogSurface, 4 | DialogTitle, 5 | DialogContent, 6 | DialogActions, 7 | DialogTrigger, 8 | DialogBody, 9 | Button, 10 | } from '@fluentui/react-components'; 11 | import Mousetrap from 'mousetrap'; 12 | import { useTranslation } from 'react-i18next'; 13 | import React, { useCallback, useEffect } from 'react'; 14 | 15 | export default function ConfirmDialog(args: { 16 | open: boolean; 17 | setOpen: (open: boolean) => void; 18 | onConfirm: () => void; 19 | title?: string; 20 | message?: string; 21 | }) { 22 | const { open, setOpen, onConfirm, title, message } = args; 23 | const confirmButtonRef = React.useRef(null); 24 | const { t } = useTranslation(); 25 | const confirm = useCallback(() => { 26 | async function confirmAndClose() { 27 | await onConfirm(); 28 | setOpen(false); 29 | } 30 | confirmAndClose(); 31 | }, [setOpen, onConfirm]); 32 | 33 | useEffect(() => { 34 | if (open) { 35 | setTimeout(() => confirmButtonRef.current?.focus(), 200); 36 | Mousetrap.bind('esc', () => setOpen(false)); 37 | } 38 | return () => { 39 | Mousetrap.unbind('esc'); 40 | }; 41 | }, [open]); 42 | 43 | return ( 44 | 45 | 46 | 47 | {title || t('Common.DeleteConfirmation')} 48 | 49 |
50 | {message || t('Common.DeleteConfirmationInfo')} 51 |
52 |
53 | 54 | 55 | 58 | 59 | 60 | 67 | 68 | 69 |
70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/renderer/components/Empty.tsx: -------------------------------------------------------------------------------- 1 | import { Image, Text } from '@fluentui/react-components'; 2 | import { t } from 'i18next'; 3 | import useAppearanceStore from 'stores/useAppearanceStore'; 4 | import { getImage } from 'renderer/components/Assets'; 5 | 6 | export default function Empty({ 7 | image, 8 | text = '', 9 | }: { 10 | image: string; 11 | text?: string; 12 | }) { 13 | const theme = useAppearanceStore((state) => state.theme); 14 | const darkImg = getImage(image, 'dark'); 15 | const lightImag = getImage(image, 'light'); 16 | return ( 17 |
18 | 19 | 24 | {t('Hint')} 30 | 31 |
32 | 33 | {text} 34 | 35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/renderer/components/ListInput.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input, InputOnChangeData } from '@fluentui/react-components'; 2 | import { AddCircleRegular, SubtractCircleRegular } from '@fluentui/react-icons'; 3 | import { ChangeEvent, useCallback, useEffect, useState } from 'react'; 4 | 5 | export default function ListInput({ 6 | placeholder, 7 | onChange, 8 | }: { 9 | label: string; 10 | placeholder: string; 11 | onChange: (value: string[]) => void; 12 | }) { 13 | const [inputValue, setInputValue] = useState(''); 14 | const [value, setValue] = useState([]); 15 | 16 | const add = useCallback(() => { 17 | if (inputValue.trim() !== '') { 18 | setValue((state) => [...state, inputValue]); 19 | setInputValue(''); 20 | } 21 | }, [inputValue]); 22 | 23 | const del = (idx: number) => { 24 | setValue((state) => state.filter((_, i) => i !== idx)); 25 | }; 26 | 27 | useEffect(() => { 28 | let val = []; 29 | if (inputValue.trim()) { 30 | val.push(inputValue.trim()); 31 | } 32 | if (value.length) { 33 | val = val.concat(value); 34 | } 35 | onChange(val); 36 | }, [inputValue, value]); 37 | 38 | useEffect(() => { 39 | return () => { 40 | setInputValue(''); 41 | setValue([]); 42 | }; 43 | }, []); 44 | 45 | return ( 46 |
47 |
48 | { 53 | if (evt.key === 'Enter') { 54 | add(); 55 | } 56 | }} 57 | onChange={(_: ChangeEvent, data: InputOnChangeData) => { 58 | setInputValue(data.value); 59 | }} 60 | /> 61 |
63 |
64 | {value.map((val: string, idx: number) => { 65 | return ( 66 |
70 | {val} 71 |
80 | ); 81 | })} 82 |
83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/renderer/components/MaskableInput.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input } from '@fluentui/react-components'; 2 | import { Eye20Filled, EyeOff20Filled } from '@fluentui/react-icons'; 3 | import { forwardRef, useState } from 'react'; 4 | 5 | function MaskableInput(props: any, ref: any) { 6 | const [showRaw, setShowRaw] = useState(false); 7 | return ( 8 | } 16 | appearance="subtle" 17 | onClick={() => setShowRaw(false)} 18 | /> 19 | ) : ( 20 | 19 | ); 20 | } 21 | 22 | export default forwardRef(StateButton); 23 | -------------------------------------------------------------------------------- /src/renderer/components/StateInput.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input, Tooltip } from '@fluentui/react-components'; 2 | import { FluentIcon, Warning20Regular } from '@fluentui/react-icons'; 3 | import { forwardRef } from 'react'; 4 | import useAppearanceStore from 'stores/useAppearanceStore'; 5 | 6 | function StateInput( 7 | props: { isValid: boolean; errorMsg: string; icon: FluentIcon } & any, 8 | ref: any, 9 | ) { 10 | const getPalette = useAppearanceStore((state) => state.getPalette); 11 | const { isValid, errorMsg, icon, ...rest } = props; 12 | return ( 13 | 20 | 61 | 62 | ); 63 | }); 64 | } 65 | return ( 66 |
67 | {collapsed ? null : t('Bookmarks.Hint.Favorites')} 68 |
69 | ); 70 | }; 71 | 72 | return ( 73 |
74 |
77 | {renderFavorites()} 78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/renderer/fluentui.scss: -------------------------------------------------------------------------------- 1 | /** override fluent css to fix some issues*/ 2 | .fui-MenuButton__menuIcon { 3 | margin-top: -5px !important; 4 | } 5 | 6 | .fui-MenuDivider { 7 | border-width: 0.04rem !important; 8 | --tw-border-opacity: 1; 9 | border-color: rgba(var(--color-border), var(--tw-border-opacity)) !important; 10 | } 11 | .fui-Listbox { 12 | max-height: 360px !important; 13 | } 14 | .fui-Label { 15 | font-family: var(--fontFamilyBase); 16 | } 17 | .field-small .fui-Label { 18 | font-size: var(--fontSizeBase200); 19 | } 20 | .toast-content { 21 | overflow-wrap: break-word; 22 | /* Instead use this non-standard one: */ 23 | word-break: break-word; 24 | white-space: pre-wrap !important; /* css-3 */ 25 | word-wrap: break-word !important; 26 | flex-wrap: wrap !important; 27 | line-height: 1.5 !important; 28 | -ms-hyphens: auto; 29 | -moz-hyphens: auto; 30 | -webkit-hyphens: auto; 31 | hyphens: auto; 32 | } 33 | 34 | .fui-PopoverSurface { 35 | max-width: 400px; 36 | } 37 | .fui-Field__validationMessage{ 38 | @apply flex justify-start items-start; 39 | } 40 | .fui-AccordionHeader__button { 41 | --fontFamilyBase: 42 | Barlow, -apple-system, BlinkMacSystemFont, PingFang SC, Hiragino Sans GB, 43 | Roboto, helvetica neue, helvetica, segoe ui, Arial, sans-serif !important; 44 | } 45 | .collapsed .fui-AccordionHeader__button { 46 | padding: 0 18px !important; 47 | } 48 | .collapsed .fui-AccordionHeader__button > span { 49 | padding-right: 0 !important; 50 | } 51 | .chat-nav button.fui-AccordionHeader__button { 52 | min-height: 32px !important; 53 | } 54 | .provider-form .fui-Checkbox__indicator { 55 | margin: 4px !important; 56 | } 57 | -------------------------------------------------------------------------------- /src/renderer/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import LanguageDetector from 'i18next-browser-languagedetector'; 4 | import resourcesToBackend from 'i18next-resources-to-backend'; 5 | 6 | i18n 7 | .use( 8 | resourcesToBackend( 9 | (language: string, namespace: string) => 10 | import(`../../public/locales/${language}/${namespace}.json`), 11 | ), 12 | ) 13 | // detect user language 14 | // learn more: https://github.com/i18next/i18next-browser-languageDetector 15 | .use(LanguageDetector) 16 | // pass the i18n instance to react-i18next. 17 | .use(initReactI18next) 18 | // init i18next 19 | // for all options read: https://www.i18next.com/overview/configuration-options 20 | .init({ 21 | fallbackLng: 'en', 22 | debug: false, 23 | defaultNS: 'translation', 24 | interpolation: { 25 | escapeValue: false, // not needed for react as it escapes by default 26 | }, 27 | }); 28 | 29 | export default i18n; 30 | -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 5ire 11 | 37 | 38 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import App from './App'; 3 | import './i18n'; 4 | 5 | const container = document.getElementById('root') as HTMLElement; 6 | const root = createRoot(container); 7 | root.render(); 8 | 9 | // calling IPC exposed from preload script 10 | window.electron.ipcRenderer.once('ipc-5ire', (arg: any) => { 11 | // eslint-disable-next-line no-console 12 | localStorage.setItem('theme', arg.darkMode ? 'dark' : 'light'); 13 | }); 14 | -------------------------------------------------------------------------------- /src/renderer/logging.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/electron/renderer'; 2 | import { init as reactInit } from '@sentry/react'; 3 | 4 | export function init() { 5 | if (window.envVars.SENTRY_DSN && window.envVars.NODE_ENV !== 'development') { 6 | Sentry.init( 7 | { 8 | dsn: window.envVars.SENTRY_DSN, 9 | }, 10 | reactInit, 11 | ); 12 | } 13 | } 14 | 15 | export function captureException(error: Error | string) { 16 | console.error(error); 17 | if (window.envVars.SENTRY_DSN && window.envVars.NODE_ENV !== 'development') { 18 | Sentry.captureException(error); 19 | } 20 | } 21 | 22 | export function captureWarning(warning: any) { 23 | console.warn(warning); 24 | if (window.envVars.SENTRY_DSN && window.envVars.NODE_ENV !== 'development') { 25 | Sentry.captureMessage(warning, 'warning'); 26 | } 27 | } 28 | 29 | export function debug(...messages: any[]) { 30 | console.debug(messages); 31 | } 32 | 33 | export function info(...messages: any[]) { 34 | console.info(...messages); 35 | } 36 | 37 | export function warn(...messages: any[]) { 38 | console.warn(...messages); 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/pages/apps/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | import { Image } from '@fluentui/react-components'; 3 | import useNav from 'hooks/useNav'; 4 | import Empty from '../../components/Empty'; 5 | import apps from '../../apps'; 6 | import { IAppConfig } from '../../apps/types'; 7 | 8 | export default function Profile() { 9 | const { t } = useTranslation(); 10 | const navigate = useNav(); 11 | return ( 12 |
13 |
14 |
15 |
16 |

{t('Common.Apps')}

17 |
18 |
19 |
20 | {t('Apps.Description')} 21 |
22 |
23 | {apps.length > 0 ? ( 24 |
25 | {apps.map((app: IAppConfig) => ( 26 |
navigate(`/apps/${app.key}`)} 29 | key={app.key} 30 | > 31 |
32 |
33 | 34 |
35 |
36 | {app.name} 37 |
38 |
39 | {app.description} 40 |
41 |
42 |
43 | ))} 44 |
45 | ) : ( 46 | 47 | )} 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/pages/bookmark/Bookmark.scss: -------------------------------------------------------------------------------- 1 | .bookmark-item{ 2 | cursor:default; 3 | } 4 | .bookmark-item:hover { 5 | --tw-bg-opacity: 0.6; 6 | border-color:rgba(var(--color-bg-surface-2), var(--tw-bg-opacity)); 7 | } 8 | 9 | .bookmark-topbar { 10 | --tw-bg-opacity: 1; 11 | background-color: rgba(var(--color-bg-sidebar), var(--tw-bg-opacity)); 12 | } 13 | [data-theme='dark'] .bookmark :not(.think):not(.think *) { 14 | color: #d6d6d6; 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/pages/chat/CitationDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogSurface, 4 | DialogBody, 5 | DialogTitle, 6 | DialogContent, 7 | DialogTrigger, 8 | Button, 9 | } from '@fluentui/react-components'; 10 | import { Dismiss24Regular } from '@fluentui/react-icons'; 11 | import { useMemo } from 'react'; 12 | import { useTranslation } from 'react-i18next'; 13 | import useKnowledgeStore from 'stores/useKnowledgeStore'; 14 | 15 | export default function CitationDialog() { 16 | const { citation } = useKnowledgeStore(); 17 | 18 | const isOpen = useMemo(() => { 19 | return citation.open; 20 | }, [citation.open]); 21 | 22 | const close = () => { 23 | useKnowledgeStore.getState().hideCitation(); 24 | }; 25 | 26 | const { t } = useTranslation(); 27 | return ( 28 | 29 | 30 | 31 | 34 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/pages/chat/Editor/Toolbar/StreamCtrl.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Switch, 3 | SwitchOnChangeData, 4 | Popover, 5 | PopoverSurface, 6 | PopoverTrigger, 7 | PopoverProps, 8 | Button, 9 | } from '@fluentui/react-components'; 10 | import { useState, ChangeEvent, useEffect } from 'react'; 11 | import { useTranslation } from 'react-i18next'; 12 | import useChatStore from 'stores/useChatStore'; 13 | import Debug from 'debug'; 14 | 15 | import { IChat, IChatContext } from 'intellichat/types'; 16 | import { Stream20Filled } from '@fluentui/react-icons'; 17 | 18 | const debug = Debug('5ire:pages:chat:Editor:Toolbar:StreamCtrl'); 19 | 20 | export default function StreamCtrl({ 21 | ctx, 22 | chat, 23 | }: { 24 | ctx: IChatContext; 25 | chat: IChat; 26 | }) { 27 | const { t } = useTranslation(); 28 | 29 | const editStage = useChatStore((state) => state.editStage); 30 | const [stream, setStream] = useState(true); 31 | 32 | const updateStream = async ( 33 | ev: ChangeEvent, 34 | data: SwitchOnChangeData, 35 | ) => { 36 | const $stream = data.checked; 37 | await editStage(chat.id, { stream: $stream }); 38 | window.electron.ingestEvent([ 39 | { app: 'toggle-stream', stream: $stream ? 'on' : 'off' }, 40 | ]); 41 | }; 42 | 43 | const [open, setOpen] = useState(false); 44 | 45 | const handleOpenChange: PopoverProps['onOpenChange'] = (e, data) => 46 | setOpen(data.open || false); 47 | 48 | const renderLabel = () => { 49 | return ( 50 | 55 | ); 56 | }; 57 | 58 | useEffect(() => { 59 | setStream(ctx.isStream()); 60 | }, [ctx]); 61 | 62 | return ( 63 | 64 | 65 | 75 | 76 | 77 |
78 | 83 |
84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/renderer/pages/chat/Editor/Toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Toolbar } from '@fluentui/react-components'; 2 | import useChatStore from 'stores/useChatStore'; 3 | import { IChat, IChatContext } from 'intellichat/types'; 4 | import ModelCtrl from './ModelCtrl'; 5 | import PromptCtrl from './PromptCtrl'; 6 | import TemperatureCtrl from './TemperatureCtrl'; 7 | import MaxTokensCtrl from './MaxTokensCtrl'; 8 | import ImgCtrl from './ImgCtrl'; 9 | import KnowledgeCtrl from './KnowledgeCtrl'; 10 | import CtxNumCtrl from './CtxNumCtrl'; 11 | 12 | export default function EditorToolbar({ 13 | ctx, 14 | isReady, 15 | onConfirm, 16 | }: { 17 | ctx: IChatContext; 18 | isReady: boolean; 19 | onConfirm: () => void; 20 | }) { 21 | const chat = useChatStore((state) => state.chat) as IChat; 22 | return ( 23 |
24 | 29 | 30 |
31 | 32 | 33 | 39 |
40 | 41 |
42 |
43 | 44 |
45 | 46 |
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/pages/chat/Messages.tsx: -------------------------------------------------------------------------------- 1 | import { IChatMessage } from 'intellichat/types'; 2 | import Message from './Message'; 3 | 4 | export default function Messages({ messages }: { messages: IChatMessage[] }) { 5 | return ( 6 |
7 | {messages.map((msg: IChatMessage) => { 8 | return ; 9 | })} 10 |
 
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/pages/knowledge/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@fluentui/react-components'; 2 | import useNav from 'hooks/useNav'; 3 | import { useEffect, useRef, useState } from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | import Empty from 'renderer/components/Empty'; 6 | import { ICollection } from 'types/knowledge'; 7 | import useKnowledgeStore from 'stores/useKnowledgeStore'; 8 | import { debounce } from 'lodash'; 9 | import Grid from './Grid'; 10 | 11 | export default function Knowledge() { 12 | const { t } = useTranslation(); 13 | const navigate = useNav(); 14 | const { listCollections, collectionChangedAt } = useKnowledgeStore(); 15 | const [collections, setCollections] = useState([]); 16 | 17 | const debouncedLoad = useRef( 18 | debounce( 19 | () => { 20 | listCollections().then((collections: ICollection[]) => { 21 | setCollections(collections); 22 | }); 23 | }, 24 | 1000, 25 | { leading: true }, 26 | ), 27 | ).current; 28 | 29 | useEffect(() => { 30 | debouncedLoad(); 31 | }, [collectionChangedAt]); 32 | 33 | return ( 34 |
35 |
36 |
37 |
38 |

39 | {t('Common.Knowledge')} 40 |

41 |
42 | 48 |
49 |
50 |
51 |
52 | {collections.length ? ( 53 |
54 | 55 |
56 | ) : ( 57 | 58 | )} 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/pages/prompt/index.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Button, InputOnChangeData } from '@fluentui/react-components'; 2 | import { Search24Regular } from '@fluentui/react-icons'; 3 | import useNav from 'hooks/useNav'; 4 | import { ChangeEvent, useEffect, useState } from 'react'; 5 | import { useTranslation } from 'react-i18next'; 6 | import Empty from 'renderer/components/Empty'; 7 | import usePromptStore from 'stores/usePromptStore'; 8 | import Grid from './Grid'; 9 | 10 | export default function Prompts() { 11 | const { t } = useTranslation(); 12 | const navigate = useNav(); 13 | const prompts = usePromptStore((state) => state.prompts); 14 | const fetchPrompts = usePromptStore((state) => state.fetchPrompts); 15 | const [keyword, setKeyword] = useState(''); 16 | useEffect(() => { 17 | fetchPrompts({ keyword }); 18 | }, [keyword, fetchPrompts]); 19 | 20 | const onKeywordChange = ( 21 | ev: ChangeEvent, 22 | data: InputOnChangeData, 23 | ) => { 24 | setKeyword(data.value || ''); 25 | }; 26 | return ( 27 |
28 |
29 |
30 |
31 |

{t('Common.Prompts')}

32 |
33 | 39 | } 41 | placeholder={t('Common.Search')} 42 | value={keyword} 43 | onChange={onKeywordChange} 44 | style={{ maxWidth: 288 }} 45 | className="flex-grow flex-shrink" 46 | /> 47 |
48 |
49 |
50 |
51 | {prompts.length ? ( 52 |
53 | 54 |
55 | ) : ( 56 | 57 | )} 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/pages/providers/CapabilityTag.tsx: -------------------------------------------------------------------------------- 1 | import { capitalize, isNil } from 'lodash'; 2 | import { IChatModelConfig } from 'providers/types'; 3 | import { useMemo } from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | export default function CapabilityTag( 7 | props: { 8 | model: IChatModelConfig; 9 | capability: 'json' | 'tools' | 'vision'; 10 | } & any, 11 | ) { 12 | const { model, capability: capabilityName } = props; 13 | 14 | const capability = useMemo(() => { 15 | return ( 16 | model.capabilities[capabilityName as keyof typeof model.capabilities] || 17 | null 18 | ); 19 | }, [model]); 20 | 21 | const originalSupport = useMemo(() => { 22 | if (isNil(capability)) return false; 23 | return true; 24 | }, [capability]); 25 | 26 | const actualSupport = useMemo(() => { 27 | return capability?.enabled || false; 28 | }, [capability]); 29 | 30 | const { t } = useTranslation(); 31 | 32 | const tagColorCls = useMemo(() => { 33 | return ( 34 | { 35 | json: 'bg-teal-50 dark:bg-teal-900 text-teal-600 dark:text-teal-300', 36 | tools: 37 | 'bg-[#d8e6f1] dark:bg-[#365065] text-[#546576] dark:text-[#e3e9e5]', 38 | vision: 39 | 'bg-[#e6ddee] dark:bg-[#4e3868] text-[#9e7ebd] dark:text-[#d9d4de]', 40 | } as { [key: string]: string } 41 | )[capabilityName]; 42 | }, [capabilityName]); 43 | 44 | const dotColorCls = useMemo(() => { 45 | return ( 46 | { 47 | json: 'bg-teal-400 bg:text-teal-600', 48 | tools: 'bg-[#546576] bg:text-[#46799f]', 49 | vision: 'bg-[#9e7ebd] bg:text-[#8d60c3]', 50 | } as { [key: string]: string } 51 | )[capabilityName]; 52 | }, [capabilityName]); 53 | 54 | return originalSupport ? ( 55 |
59 |
67 | {t(`Tags.${capitalize(capabilityName)}`)} 68 |
69 | ) : null; 70 | } 71 | -------------------------------------------------------------------------------- /src/renderer/pages/providers/ToolTag.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from '@fluentui/react-components'; 2 | import { isUndefined } from 'lodash'; 3 | import { useMemo } from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | import useProviderStore from 'stores/useProviderStore'; 6 | import useSettingsStore from 'stores/useSettingsStore'; 7 | 8 | export default function ToolTag( 9 | props: { 10 | providerName: string; 11 | modelName: string; 12 | } & any, 13 | ) { 14 | const { providerName, modelName, ...rest } = props; 15 | const { getToolState } = useSettingsStore(); 16 | const { providers } = useProviderStore(); 17 | 18 | const originalSupport = useMemo(() => { 19 | const provider = providers[providerName]; 20 | if (!provider) return false; 21 | const model = provider.models.find((m) => m.name === modelName); 22 | if (!model) return false; 23 | return !!model.capabilities?.tools || false; 24 | }, [providerName, modelName]); 25 | 26 | const actualSupport = useMemo(() => { 27 | let toolState = getToolState(providerName, modelName); 28 | if (isUndefined(toolState)) { 29 | toolState = originalSupport; 30 | } 31 | return toolState; 32 | }, [providerName, modelName, originalSupport]); 33 | 34 | const { t } = useTranslation(); 35 | const tip = t(actualSupport ? 'Tool.Supported' : 'Tool.NotSupported'); 36 | 37 | return ( 38 | 47 |
48 | {t('Tags.Tools')} 49 | 56 | ● 57 | 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/pages/settings/AppearanceSettings.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { 4 | RadioGroup, 5 | Radio, 6 | RadioGroupOnChangeData, 7 | } from '@fluentui/react-components'; 8 | import { set } from 'lodash'; 9 | import { captureException } from '../../logging'; 10 | import { FontSize, ThemeType } from '../../../types/appearance.d'; 11 | import useSettingsStore from '../../../stores/useSettingsStore'; 12 | import useAppearanceStore from '../../../stores/useAppearanceStore'; 13 | 14 | export default function AppearanceSettings() { 15 | const { t } = useTranslation(); 16 | const { setTheme } = useAppearanceStore(); 17 | const fontSize = useSettingsStore((state) => state.fontSize); 18 | const themeSetting = useSettingsStore((state) => state.theme); 19 | const setThemeSetting = useSettingsStore((state) => state.setTheme); 20 | const setFontSize = useSettingsStore((state) => state.setFontSize); 21 | 22 | const onThemeChange = ( 23 | ev: FormEvent, 24 | data: RadioGroupOnChangeData, 25 | ) => { 26 | setThemeSetting(data.value as ThemeType); 27 | if (data.value === 'system') { 28 | window.electron 29 | .getNativeTheme() 30 | .then((_theme) => { 31 | return setTheme(_theme as ThemeType); 32 | }) 33 | .catch(captureException); 34 | } else { 35 | setTheme(data.value as ThemeType); 36 | } 37 | }; 38 | 39 | const onFontSizeChange = ( 40 | ev: FormEvent, 41 | data: RadioGroupOnChangeData, 42 | ) => { 43 | setFontSize(data.value as FontSize); 44 | }; 45 | return ( 46 |
47 |
{t('Common.Appearance')}
48 |
49 |

{t('Appearance.ColorTheme')}

50 | 56 | 57 | 58 | 63 | 64 |
65 |
66 |

{t('Appearance.ChatFontSize')}

67 | 73 | 74 | 75 | 76 | 77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/renderer/pages/settings/LanguageSettings.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { 4 | RadioGroup, 5 | Radio, 6 | RadioGroupOnChangeData, 7 | } from '@fluentui/react-components'; 8 | import { captureException } from '../../logging'; 9 | import { LanguageType } from '../../../types/settings.d'; 10 | import useSettingsStore from '../../../stores/useSettingsStore'; 11 | 12 | export default function LanguageSettings() { 13 | const { t, i18n } = useTranslation(); 14 | const language = useSettingsStore((state) => state.language); 15 | const setLanguage = useSettingsStore((state) => state.setLanguage); 16 | 17 | const onLanguageChange = ( 18 | ev: FormEvent, 19 | data: RadioGroupOnChangeData, 20 | ) => { 21 | setLanguage(data.value as LanguageType); 22 | if (data.value === 'system') { 23 | window.electron 24 | .getSystemLanguage() 25 | .then((_lang) => { 26 | i18n.changeLanguage(_lang as LanguageType); 27 | }) 28 | .catch(captureException); 29 | } else { 30 | i18n.changeLanguage(data.value as LanguageType); 31 | } 32 | setLanguage(data.value as LanguageType); 33 | }; 34 | return ( 35 |
36 |
{t('Common.Language')}
37 |
38 | 43 | 44 | 49 | 54 | 55 |
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/renderer/pages/settings/Settings.scss: -------------------------------------------------------------------------------- 1 | .settings-section { 2 | @apply flex; 3 | @apply border-t; 4 | @apply mt-2.5; 5 | --tw-border-opacity: 1; 6 | border-color: rgba(var(--color-border), var(--tw-border-opacity)); 7 | } 8 | 9 | .settings-section--header { 10 | @apply flex-grow-0; 11 | @apply px-2.5; 12 | @apply py-5; 13 | @apply w-28; 14 | @apply font-semibold; 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/pages/settings/Version.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import Spinner from 'renderer/components/Spinner'; 4 | import { captureException } from '../../logging'; 5 | 6 | interface IUpdateInfo { 7 | version: string; 8 | releaseNotes: string; 9 | releaseName: string; 10 | isDownloading: boolean; 11 | } 12 | 13 | export default function Version() { 14 | const { t } = useTranslation(); 15 | 16 | const [updateInfo, setUpdateInfo] = useState(); 17 | const [version, setVersion] = useState('0'); 18 | 19 | useEffect(() => { 20 | let timer: number | null = null; 21 | let info = window.electron.store.get('updateInfo'); 22 | setUpdateInfo(info); 23 | if (info?.isDownloading) { 24 | timer = setInterval(() => { 25 | info = window.electron.store.get('updateInfo'); 26 | if (timer && !info?.isDownloading) { 27 | clearInterval(timer); 28 | } 29 | setUpdateInfo(info); 30 | }, 1000) as any; 31 | } 32 | window.electron 33 | .getAppVersion() 34 | .then((appVersion) => { 35 | return setVersion(appVersion); 36 | }) 37 | .catch(captureException); 38 | 39 | return () => { 40 | if (timer) { 41 | clearInterval(timer); 42 | } 43 | }; 44 | }, []); 45 | 46 | return ( 47 |
48 |
{t('Common.Version')}
49 |
50 |
{version}
51 | {updateInfo && ( 52 |
53 | {updateInfo?.isDownloading ? ( 54 | <> 55 |
{t('Version.HasNewVersion')}
56 |
57 | 58 | {t('Common.Downloading')} 59 |
60 | 61 | ) : ( 62 |
63 | {updateInfo?.version} will be installed after you restart the 64 | app. 65 |
66 | )} 67 |
68 | )} 69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/renderer/pages/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | 3 | import './Settings.scss'; 4 | 5 | import { Link } from 'react-router-dom'; 6 | import Version from './Version'; 7 | import AppearanceSettings from './AppearanceSettings'; 8 | import EmbedSettings from './EmbedSettings'; 9 | import LanguageSettings from './LanguageSettings'; 10 | 11 | export default function Settings() { 12 | const { t } = useTranslation(); 13 | 14 | return ( 15 |
16 |
17 |
18 |
19 |

{t('Common.Settings')}

20 |
21 |
22 |
23 |
24 |
{t('Common.API')}
25 |
26 | 27 | {t('Settings.ProviderSettingsMovedTo')}  28 | 29 | 30 | {t('Common.Providers')} 31 | 32 |
33 |
34 | 35 | 36 | 37 | 38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/pages/tool/NewButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogTrigger, 4 | DialogSurface, 5 | DialogTitle, 6 | DialogContent, 7 | DialogBody, 8 | Button, 9 | } from '@fluentui/react-components'; 10 | import { useEffect, useState } from 'react'; 11 | import { useTranslation } from 'react-i18next'; 12 | 13 | export default function NewButton() { 14 | const { t } = useTranslation(); 15 | const [configFilePath, setConfigFilePath] = useState(''); 16 | 17 | useEffect(() => { 18 | window.electron.getUserDataPath(['mcp.json']).then((path: string) => { 19 | setConfigFilePath(path); 20 | }); 21 | }); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | {t('Tool.HowToAddTools')} 31 | 32 |

{t('Tool.HowToAddToolsDescription')}

33 |
34 |
{t('Tool.ConfigFile')}
35 | {configFilePath} 36 |
37 |
38 |
39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/pages/user/TabPassword.tsx: -------------------------------------------------------------------------------- 1 | import { Field, InputOnChangeData } from '@fluentui/react-components'; 2 | import { Password20Regular } from '@fluentui/react-icons'; 3 | import useToast from 'hooks/useToast'; 4 | import { ChangeEvent, useState } from 'react'; 5 | import { useTranslation } from 'react-i18next'; 6 | import MaskableStateInput from 'renderer/components/MaskableStateInput'; 7 | import StateButton from 'renderer/components/StateButton'; 8 | import { isValidPassword } from 'utils/validators'; 9 | import supabase from 'vendors/supa'; 10 | 11 | export default function TabPassword() { 12 | const { t } = useTranslation(); 13 | const [loading, setLoading] = useState(false); 14 | const [password, setPassword] = useState(''); 15 | const [isPasswordValid, setIsPasswordValid] = useState(true); 16 | 17 | const { notifyError, notifySuccess } = useToast(); 18 | 19 | const updatePassword = async () => { 20 | if (!isValidPassword(password)) return; 21 | setLoading(true); 22 | const { error } = await supabase.auth.updateUser({ password }); 23 | if (error) { 24 | notifyError(error.message); 25 | } else { 26 | notifySuccess(t('Account.Notification.PasswordChanged')); 27 | setIsPasswordValid(true); 28 | setPassword(''); 29 | } 30 | setLoading(false); 31 | }; 32 | return ( 33 |
34 |
35 | {t('Common.ChangePassword')} 36 |
37 | 38 | { 41 | setIsPasswordValid(isValidPassword(password)); 42 | }} 43 | isValid={isPasswordValid} 44 | icon={} 45 | errorMsg={t('Account.Info.PasswordRule')} 46 | value={password} 47 | onChange={( 48 | _ev: ChangeEvent, 49 | data: InputOnChangeData, 50 | ) => { 51 | setPassword(data.value); 52 | setIsPasswordValid(true); 53 | }} 54 | /> 55 | 56 |
{t('Account.Info.PasswordRule')}
57 |
58 | 63 | {t('Common.Save')} 64 | 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/renderer/preload.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronHandler, EnvVars } from 'main/preload'; 2 | 3 | declare global { 4 | // eslint-disable-next-line no-unused-vars 5 | interface Window { 6 | electron: ElectronHandler; 7 | envVars: EnvVars; 8 | } 9 | } 10 | 11 | export {}; 12 | -------------------------------------------------------------------------------- /src/renderer/variables.scss: -------------------------------------------------------------------------------- 1 | [data-theme='light'] { 2 | --color-bg-base: 255, 255, 255; 3 | --color-bg-surface-1: 255, 255, 255; 4 | --color-bg-surface-2: 243, 244, 246; 5 | --color-bg-surface-3: 220, 220, 220; 6 | --color-border: 224, 224, 224; 7 | --color-border-secondary: 180, 180, 180; 8 | --color-bg-sidebar: 238, 236, 235; 9 | --color-accent: 63, 118, 255; 10 | --color-text-base: 3, 7, 18; 11 | --color-text-secondary: 55, 65, 81; 12 | --color-text-success: 61, 125, 63; 13 | --color-text-warning: 217, 137, 38; 14 | --color-text-danger: 198, 71, 78; 15 | --color-text-info: 102, 109, 117; 16 | color-scheme: light; 17 | } 18 | 19 | [data-theme='light-contrast'] { 20 | --color-bg-base: 250, 250, 250; 21 | --color-bg-surface-1: 250, 250, 250; 22 | --color-bg-surface-2: 206, 212, 218; 23 | --color-bg-surface-3: 220, 220, 220; 24 | --color-border: 0, 0, 0; 25 | --color-border-secondary: 50,50,50; 26 | --color-bg-sidebar: 255, 255, 255; 27 | --color-accent: 63, 118, 255; 28 | --color-text-base: 0, 0, 0; 29 | --color-text-secondary: 0, 0, 0; 30 | --color-text-success: 61, 125, 63; 31 | --color-text-warning: 217, 137, 38; 32 | --color-text-danger: 198, 71, 78; 33 | --color-text-info: 102, 109, 117; 34 | color-scheme: light; 35 | } 36 | 37 | [data-theme='dark'] { 38 | --color-bg-base: 25, 27, 27; 39 | --color-bg-surface-1: 46, 46, 46; 40 | --color-bg-surface-2: 48, 49, 52; 41 | --color-bg-surface-3: 49, 54, 58; 42 | --color-border: 36, 36, 36; 43 | --color-border-secondary: 28,28,28; 44 | --color-bg-sidebar: 44, 42, 43; 45 | --color-accent: 60, 133, 217; 46 | --color-text-base: 200, 214, 222; 47 | --color-text-secondary: 142, 148, 146; 48 | --color-text-success: 67, 132, 64; 49 | --color-text-warning: 230, 165, 42; 50 | --color-text-danger: 231, 121, 117; 51 | --color-text-info: 126, 133, 143; 52 | color-scheme: dark; 53 | } 54 | 55 | [data-theme='dark-contrast'] { 56 | --color-bg-base: 25, 27, 27; 57 | --color-bg-surface-1: 31, 32, 35; 58 | --color-bg-surface-2: 39, 42, 45; 59 | --color-border: 255, 255, 255; 60 | --color-border-secondary: 210,210,210; 61 | --color-bg-sidebar: 19, 20, 22; 62 | --color-accent: 60, 133, 217; 63 | --color-text-base: 255, 255, 255; 64 | --color-text-secondary: 142, 148, 146; 65 | --color-text-success: 67, 132, 64; 66 | --color-text-warning: 230, 165, 42; 67 | --color-text-danger: 231, 121, 117; 68 | --color-text-info: 126, 133, 143; 69 | color-scheme: dark; 70 | } 71 | -------------------------------------------------------------------------------- /src/stores/useAppearanceStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { ThemeType } from 'types/appearance'; 3 | 4 | const defaultTheme = 'light'; 5 | 6 | interface IAppearanceStore { 7 | theme: Omit; 8 | sidebar: { 9 | hidden: boolean; 10 | collapsed: boolean; 11 | }; 12 | chatSidebar: { 13 | show: boolean; 14 | }; 15 | setTheme: (theme: Omit) => void; 16 | toggleSidebarCollapsed: () => void; 17 | toggleSidebarVisibility: () => void; 18 | toggleChatSidebarVisibility: () => void; 19 | getPalette: (name: 'success' | 'warning' | 'error' | 'info') => string; 20 | } 21 | 22 | const useAppearanceStore = create((set, get) => ({ 23 | theme: defaultTheme, 24 | sidebar: { 25 | hidden: localStorage.getItem('sidebar-hidden') === 'true', 26 | collapsed: localStorage.getItem('sidebar-collapsed') === 'true', 27 | }, 28 | chatSidebar: { 29 | show: localStorage.getItem('chat-sidebar-show') === 'true', 30 | }, 31 | setTheme: (theme: Omit) => { 32 | if (theme === 'dark') { 33 | document.documentElement.classList.add('dark'); 34 | } else { 35 | document.documentElement.classList.remove('dark'); 36 | } 37 | set({ theme }); 38 | }, 39 | toggleSidebarCollapsed: () => { 40 | set((state) => { 41 | const collapsed = !state.sidebar.collapsed; 42 | const hidden = false; 43 | localStorage.setItem('sidebar-collapsed', String(collapsed)); 44 | window.electron.ingestEvent([{ app: 'toggle-sidebar-collapsed' }]); 45 | return { sidebar: { collapsed, hidden } }; 46 | }); 47 | }, 48 | toggleSidebarVisibility: () => { 49 | set((state) => { 50 | const hidden = !state.sidebar.hidden; 51 | const collapsed = false; 52 | localStorage.setItem('sidebar-hidden', String(hidden)); 53 | window.electron.ingestEvent([{ app: 'toggle-sidebar-visibility' }]); 54 | return { sidebar: { collapsed, hidden } }; 55 | }); 56 | }, 57 | toggleChatSidebarVisibility: () => { 58 | set((state) => { 59 | const show = !state.chatSidebar.show; 60 | localStorage.setItem('chat-sidebar-show', String(show)); 61 | window.electron.ingestEvent([{ app: 'toggle-chat-sidebar-visibility' }]); 62 | return { chatSidebar: { show } }; 63 | }); 64 | }, 65 | getPalette: (name: 'error' | 'warning' | 'success' | 'info') => { 66 | const light = { 67 | success: '#3d7d3f', 68 | warning: '#d98926', 69 | error: '#c6474e', 70 | info: '#6e747d', 71 | }; 72 | const dark = { 73 | success: '#64b75d', 74 | warning: '#e6a52a', 75 | error: '#de5d43', 76 | info: '#e7edf2', 77 | }; 78 | const { theme } = get(); 79 | return theme === 'dark' ? dark[name] : light[name]; 80 | }, 81 | })); 82 | 83 | export default useAppearanceStore; 84 | -------------------------------------------------------------------------------- /src/stores/useInspectorStore.ts: -------------------------------------------------------------------------------- 1 | import { typeid } from 'typeid-js'; 2 | import { create } from 'zustand'; 3 | 4 | export interface ITraceMessage { 5 | id: string; 6 | label: string; 7 | message: string; 8 | } 9 | 10 | interface IInspectorStore { 11 | messages: { [key: string]: ITraceMessage[] }; 12 | trace: (chatId: string, label: string, message: string) => void; 13 | clearTrace: (chatId: string) => void; 14 | } 15 | 16 | const useInspectorStore = create((set, get) => ({ 17 | messages: {}, 18 | trace: (chatId: string, label: string, message: string) => { 19 | const { messages } = get(); 20 | const id = typeid('trace').toString(); 21 | if (!messages[chatId]) { 22 | set({ messages: { ...messages, [chatId]: [{ id, label, message }] } }); 23 | } else { 24 | set({ 25 | messages: { 26 | ...messages, 27 | [chatId]: messages[chatId].concat([{ id, label, message }]), 28 | }, 29 | }); 30 | } 31 | }, 32 | clearTrace: (chatId: string) => { 33 | const { messages } = get(); 34 | delete messages[chatId]; 35 | set({ messages: { ...messages } }); 36 | }, 37 | })); 38 | 39 | export default useInspectorStore; 40 | -------------------------------------------------------------------------------- /src/stores/useMCPServerMarketStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { IMCPServer } from '../types/mcp.d'; 3 | import { captureException } from '../renderer/logging'; 4 | 5 | const REMOTE_CONFIG_TTL: number = 1000 * 60 * 60 * 24; // 1 day 6 | 7 | interface IMCPServerMarketStore { 8 | filters: string[]; 9 | setFilters: (filters: string[]) => void; 10 | servers: IMCPServer[]; 11 | updatedAt: number; 12 | fetchServers: (force?: boolean) => Promise; 13 | } 14 | 15 | const useMCPServerMarketStore = create((set, get) => ({ 16 | filters: [], 17 | servers: [], 18 | updatedAt: 0, 19 | setFilters: (filters: string[]) => { 20 | set({ filters }); 21 | }, 22 | fetchServers: async (force = false) => { 23 | const { servers, updatedAt } = get(); 24 | if (!force && updatedAt > Date.now() - REMOTE_CONFIG_TTL) { 25 | return servers; 26 | } 27 | try { 28 | const resp = await fetch('https://mcpsvr.com/servers.json'); 29 | if (resp.ok) { 30 | const data = await resp.json(); 31 | set({ 32 | servers: data, 33 | updatedAt: Date.now(), 34 | }); 35 | return data; 36 | } 37 | captureException(resp.statusText); 38 | return []; 39 | } catch (error: any) { 40 | captureException(error); 41 | return []; 42 | } 43 | }, 44 | })); 45 | 46 | export default useMCPServerMarketStore; 47 | -------------------------------------------------------------------------------- /src/stores/useMCPStore.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { IMCPConfig, IMCPServer } from 'types/mcp'; 3 | import { create } from 'zustand'; 4 | 5 | const debug = Debug('5ire:stores:useMCPStore'); 6 | 7 | export interface IMCPStore { 8 | isLoading: boolean; 9 | config: IMCPConfig; 10 | updateLoadingState: (isLoading: boolean) => void; 11 | loadConfig: (force?: boolean) => Promise; 12 | addServer: (server: IMCPServer) => Promise; 13 | updateServer: (server: IMCPServer) => Promise; 14 | deleteServer: (key: string) => Promise; 15 | activateServer: ( 16 | key: string, 17 | command?: string, 18 | args?: string[], 19 | env?: Record, 20 | ) => Promise; 21 | deactivateServer: (key: string) => Promise; 22 | } 23 | 24 | const useMCPStore = create((set, get) => ({ 25 | isLoading: true, 26 | config: { mcpServers: {} }, 27 | updateLoadingState: (isLoading: boolean) => { 28 | set({ isLoading }); 29 | }, 30 | loadConfig: async (force?: boolean) => { 31 | if (!force && Object.keys(get().config.mcpServers).length > 0) { 32 | return get().config; 33 | } 34 | const config = await window.electron.mcp.getConfig(); 35 | set({ config }); 36 | return config; 37 | }, 38 | addServer: async (server: IMCPServer) => { 39 | const ok = await window.electron.mcp.addServer(server); 40 | if (ok) { 41 | get().loadConfig(true); 42 | return true; 43 | } 44 | return false; 45 | }, 46 | updateServer: async (server: IMCPServer) => { 47 | const ok = await window.electron.mcp.updateServer(server); 48 | if (ok) { 49 | get().loadConfig(true); 50 | return true; 51 | } 52 | return false; 53 | }, 54 | deleteServer: async (key: string) => { 55 | const { mcpServers } = get().config; 56 | const server = mcpServers[key]; 57 | if (server) { 58 | let ok = true; 59 | if (server.isActive) { 60 | ok = await get().deactivateServer(key); 61 | } 62 | if (ok) { 63 | delete mcpServers[key]; 64 | const newConfig = { mcpServers: { ...mcpServers } }; 65 | set({ config: newConfig }); 66 | await window.electron.mcp.putConfig(newConfig); 67 | return true; 68 | } 69 | } 70 | return false; 71 | }, 72 | activateServer: async ( 73 | key: string, 74 | command?: string, 75 | args?: string[], 76 | env?: Record, 77 | ) => { 78 | debug('Activating server:', { 79 | key, 80 | command, 81 | args, 82 | env, 83 | }); 84 | const { error } = await window.electron.mcp.activate({ 85 | key, 86 | command, 87 | args, 88 | env, 89 | }); 90 | if (error) { 91 | throw new Error(error); 92 | } 93 | await get().loadConfig(true); 94 | return true; 95 | }, 96 | deactivateServer: async (key: string) => { 97 | const { error } = await window.electron.mcp.deactivated(key); 98 | if (error) { 99 | throw new Error(error); 100 | } 101 | await get().loadConfig(true); 102 | return true; 103 | }, 104 | })); 105 | 106 | export default useMCPStore; 107 | -------------------------------------------------------------------------------- /src/stores/useSettingsStore.ts: -------------------------------------------------------------------------------- 1 | // import Debug from 'debug'; 2 | import { create } from 'zustand'; 3 | import { LanguageType, ISettings } from '../types/settings.d'; 4 | /* eslint-disable no-console */ 5 | 6 | import { FontSize, ThemeType } from '../types/appearance'; 7 | 8 | // const debug = Debug('5ire:stores:useSettingsStore'); 9 | 10 | const defaultTheme = 'system'; 11 | const defaultLanguage = 'system'; 12 | const defaultFontSize = 'base'; 13 | 14 | export interface ISettingStore { 15 | theme: ThemeType; 16 | language: LanguageType; 17 | fontSize: FontSize; 18 | setTheme: (theme: ThemeType) => void; 19 | setLanguage: (language: LanguageType) => void; 20 | setFontSize: (fontSize: FontSize) => void; 21 | } 22 | 23 | const settings = window.electron.store.get('settings', {}) as ISettings; 24 | 25 | const useSettingsStore = create((set) => ({ 26 | theme: settings?.theme || defaultTheme, 27 | language: settings?.language || defaultLanguage, 28 | fontSize: settings?.fontSize || defaultFontSize, 29 | setTheme: async (theme: ThemeType) => { 30 | set({ theme }); 31 | window.electron.store.set('settings.theme', theme); 32 | }, 33 | setLanguage: (language: 'en' | 'zh' | 'system') => { 34 | set({ language }); 35 | window.electron.store.set('settings.language', language); 36 | }, 37 | setFontSize: (fontSize: FontSize) => { 38 | set({ fontSize }); 39 | window.electron.store.set('settings.fontSize', fontSize); 40 | }, 41 | })); 42 | 43 | export default useSettingsStore; 44 | -------------------------------------------------------------------------------- /src/stores/useUsageStore.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { typeid } from 'typeid-js'; 3 | import { IUsage, IUsageStatistics } from 'types/usage'; 4 | import { date2unix } from 'utils/util'; 5 | import { create } from 'zustand'; 6 | import useProviderStore from './useProviderStore'; 7 | 8 | const debug = Debug('5ire:stores:useUsageStore'); 9 | 10 | export interface IUsageStore { 11 | create: (usage: Partial) => Promise; 12 | statistics: ( 13 | startDateUnix: number, 14 | endDateUnix: number, 15 | ) => Promise; 16 | } 17 | const { getAvailableModel } = useProviderStore.getState(); 18 | const getModelPrice = ( 19 | providerName: string, 20 | modelName: string, 21 | type: 'input' | 'output', 22 | ) => { 23 | if (type === 'input') { 24 | return getAvailableModel(providerName, modelName).inputPrice; 25 | } 26 | return getAvailableModel(providerName, modelName).outputPrice; 27 | }; 28 | 29 | const useUsageStore = create(() => ({ 30 | create: async (usage: Partial) => { 31 | const $usage = { 32 | ...usage, 33 | id: typeid('usg').toString(), 34 | createdAt: date2unix(new Date()), 35 | } as IUsage; 36 | debug('Create a usage ', $usage); 37 | const ok = await window.electron.db.run( 38 | `INSERT INTO usages (id, provider,model, inputTokens, outputTokens, inputPrice, outputPrice,createdAt) 39 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 40 | [ 41 | $usage.id, 42 | $usage.provider, 43 | $usage.model, 44 | $usage.inputTokens, 45 | $usage.outputTokens, 46 | getModelPrice($usage.provider, $usage.model, 'input'), 47 | getModelPrice($usage.provider, $usage.model, 'output'), 48 | $usage.createdAt, 49 | ], 50 | ); 51 | if (!ok) { 52 | throw new Error('Write the usage into database failed'); 53 | } 54 | return $usage; 55 | }, 56 | statistics: async (startDateUnix: number, endDateUnix: number) => { 57 | return (await window.electron.db.all( 58 | ` 59 | SELECT 60 | provider, 61 | model, 62 | sum(inputTokens) inputTokens, 63 | sum(outputTokens) outputTokens, 64 | round(sum(inputTokens * inputPrice / 1000), 4) AS inputCost, 65 | round(sum(outputTokens * outputPrice / 1000), 4) AS outputCost 66 | FROM 67 | usages 68 | WHERE 69 | createdAt >= ? AND createdAt <= ? 70 | GROUP BY 71 | provider, model`, 72 | [startDateUnix, endDateUnix], 73 | )) as IUsageStatistics[]; 74 | }, 75 | })); 76 | 77 | export default useUsageStore; 78 | -------------------------------------------------------------------------------- /src/types/appearance.d.ts: -------------------------------------------------------------------------------- 1 | export type ThemeType = 'light' | 'dark' | 'system'; 2 | export type FontSize = 'base' | 'large' | 'xl'; 3 | -------------------------------------------------------------------------------- /src/types/auth.d.ts: -------------------------------------------------------------------------------- 1 | export default interface IAuthingData { 2 | accessToken: string; 3 | expiresIn: string; 4 | idToken: string; 5 | scope: string; 6 | tokenType: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/bookmark.d.ts: -------------------------------------------------------------------------------- 1 | export interface IBookmark { 2 | id: string; 3 | msgId: string; 4 | prompt: string; 5 | reply: string; 6 | reasoning?: string; 7 | model: string; 8 | temperature: number; 9 | memo?: string; 10 | favorite?: boolean; 11 | citedFiles?: string; 12 | citedChunks?: string; 13 | createdAt: number; 14 | } 15 | -------------------------------------------------------------------------------- /src/types/d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | Canny: any; 3 | } 4 | -------------------------------------------------------------------------------- /src/types/device.d.ts: -------------------------------------------------------------------------------- 1 | export default interface IDeviceInfo { 2 | id: string; 3 | arch: string; 4 | platform: string; 5 | type: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/error.d.ts: -------------------------------------------------------------------------------- 1 | export interface IOpenAIError { 2 | message: string; 3 | type: string; 4 | param: string | null; 5 | code: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/knowledge.d.ts: -------------------------------------------------------------------------------- 1 | export interface ICollection { 2 | id: string; 3 | name: string; 4 | memo?: string; 5 | numOfFiles?: number; 6 | favorite?: boolean; 7 | pinedAt?: number | null; 8 | createdAt: number; 9 | updatedAt: number; 10 | } 11 | 12 | export interface ICollectionFile { 13 | id: string; 14 | collectionId: string; 15 | name: string; 16 | size: number; 17 | numOfChunks?: number; 18 | createdAt: number; 19 | updatedAt: number; 20 | } 21 | 22 | export interface IKnowledgeChunk { 23 | id: string; 24 | collectionId: string; 25 | fileId: string; 26 | content: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/types/mcp.d.ts: -------------------------------------------------------------------------------- 1 | export type MCPServerType = 'local' | 'remote'; 2 | export interface IMCPServer { 3 | key: string; 4 | type: MCPServerType; 5 | name?: string; 6 | url?: string; 7 | command?: string; 8 | description?: string; 9 | args?: string[]; 10 | env?: Record; 11 | headers?: Record; 12 | isActive: boolean; 13 | homepage?: string; 14 | } 15 | 16 | export type MCPArgType = 'string' | 'list' | 'number'; 17 | export type MCPEnvType = 'string' | 'number'; 18 | export type MCPArgParameter = { [key: string]: MCPArgType }; 19 | export type MCPEnvParameter = { [key: string]: MCPEnvType }; 20 | 21 | export interface IMCPServerParameter { 22 | name: string; 23 | type: MCPArgType | MCPEnvType; 24 | description: string; 25 | } 26 | 27 | export interface IMCPConfig { 28 | servers?: IMCPServer[]; // Deprecated 29 | mcpServers: { 30 | [key: string]: IMCPServer; 31 | }; 32 | updated?: number; 33 | } 34 | -------------------------------------------------------------------------------- /src/types/settings.d.ts: -------------------------------------------------------------------------------- 1 | import { ProviderType } from 'providers/types'; 2 | import { FontSize, ThemeType } from './appearance'; 3 | 4 | export type LanguageType = 'en' | 'zh' | 'system'; 5 | 6 | export interface IAPISettings { 7 | provider: ProviderType; 8 | base: string; 9 | key: string; 10 | model: string; 11 | secret?: string; 12 | deploymentId?: string; 13 | endpoint?: string; 14 | } 15 | 16 | export interface ISettings { 17 | theme: ThemeType; 18 | language: LanguageType; 19 | fontSize: FontSize; 20 | api: { 21 | activeProvider: string; 22 | providers: { 23 | [key: string]: IAPISettings; 24 | }; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/types/usage.d.ts: -------------------------------------------------------------------------------- 1 | export interface IUsage { 2 | id: string; 3 | provider: string; 4 | model: string; 5 | inputTokens: number; 6 | outputTokens: number; 7 | inputPrice: number; 8 | outputPrice: number; 9 | createdAt: number; 10 | } 11 | 12 | export interface IUsageStatistics { 13 | provider: string; 14 | model: string; 15 | inputTokens: number; 16 | outputTokens: number; 17 | inputCost: number; 18 | outputCost: number; 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/bus.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | 3 | export type RetryEvent = { 4 | prompt: string; 5 | messageId: string; 6 | }; 7 | 8 | const eventBus = mitt(); 9 | 10 | export default eventBus; 11 | -------------------------------------------------------------------------------- /src/utils/cache.ts: -------------------------------------------------------------------------------- 1 | function cacheKey(key: string) { 2 | return `__cache__${key}`; 3 | } 4 | 5 | export default { 6 | // default expiration is 1 hour 7 | set(key: string, value: any, expiration = 3600000) { 8 | const item = { 9 | value, 10 | expiration: Date.now() + expiration, 11 | }; 12 | localStorage.setItem(cacheKey(key), JSON.stringify(item)); 13 | }, 14 | 15 | get(key: string) { 16 | const itemStr = localStorage.getItem(key); 17 | if (!itemStr) { 18 | return null; 19 | } 20 | 21 | const item = JSON.parse(itemStr); 22 | if (Date.now() > item.expiration) { 23 | localStorage.removeItem(key); 24 | return null; 25 | } 26 | 27 | return item.value; 28 | }, 29 | 30 | remove(key: string) { 31 | localStorage.removeItem(cacheKey(key)); 32 | }, 33 | 34 | clear() { 35 | const keys = Object.keys(localStorage); 36 | for (let i = 0; i < keys.length; i++) { 37 | if (keys[i].startsWith('__cache__')) { 38 | localStorage.removeItem(keys[i]); 39 | } 40 | } 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/utils/mcp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ['--db-path',] => dbPath 3 | */ 4 | 5 | import { flatten } from 'lodash'; 6 | import { 7 | MCPArgParameter, 8 | MCPArgType, 9 | MCPEnvType, 10 | IMCPServerParameter, 11 | IMCPServer, 12 | } from 'types/mcp'; 13 | 14 | export function getParameters(params: string[]): IMCPServerParameter[] { 15 | const result: IMCPServerParameter[] = []; 16 | if (!params) { 17 | return result; 18 | } 19 | const pattern = 20 | /\{\{(?[^@]+)@(?[^:]+)(::(?[^}]*)?)?\}\}/; 21 | params.forEach((param: string) => { 22 | const match = param.match(pattern); 23 | if (match && match.groups) { 24 | result.push({ 25 | name: match.groups.name, 26 | type: match.groups.type as MCPEnvType | MCPArgType, 27 | description: match.groups.description || '', 28 | }); 29 | } 30 | }); 31 | return result; 32 | } 33 | 34 | export function fillArgs(args: string[], params: MCPArgParameter): string[] { 35 | const pattern = 36 | /\{\{(?[^@]+)@(?[^:]+)(::(?[^}]*)?)?\}\}/; 37 | const $args: (string | string[])[] = [...args]; 38 | for (let index = 0; index < args.length; index++) { 39 | const arg = args[index]; 40 | const match = arg.match(pattern); 41 | if (match && match.groups) { 42 | const paramValue = params[match.groups.name]; 43 | if (Array.isArray(paramValue)) { 44 | $args[index] = paramValue; 45 | } else { 46 | $args[index] = arg.replace(match[0], paramValue); 47 | } 48 | } 49 | } 50 | return flatten($args); 51 | } 52 | 53 | export function FillEnvOrHeaders( 54 | envOrHeaders: Record | undefined, 55 | params: { [key: string]: string }, 56 | ): Record { 57 | if (!envOrHeaders) return {}; 58 | const pattern = 59 | /\{\{(?[^@]+)@(?[^:]+)(::(?[^}]*)?)?\}\}/g; 60 | const $envOrHeaders = { ...envOrHeaders }; 61 | const keys = Object.keys(envOrHeaders); 62 | for (const key of keys) { 63 | const item = envOrHeaders[key]; 64 | let result = item; 65 | let match; 66 | while ((match = pattern.exec(item)) !== null) { 67 | if (match.groups) { 68 | const placeholder = match[0]; 69 | const paramValue = params[match.groups.name]; 70 | if (paramValue !== undefined) { 71 | result = result.replace(placeholder, paramValue); 72 | } 73 | } 74 | } 75 | 76 | $envOrHeaders[key] = result; 77 | } 78 | return $envOrHeaders; 79 | } 80 | 81 | export function purifyServer(server: IMCPServer): Omit { 82 | return { 83 | name: server.name, 84 | description: server.description, 85 | url: server.url, 86 | command: server.command, 87 | ...(server.args?.length ? { args: server.args } : {}), 88 | ...(Object.keys(server.headers || {}).length 89 | ? { headers: server.headers } 90 | : {}), 91 | ...(Object.keys(server.env || {}).length ? { env: server.env } : {}), 92 | isActive: server.isActive || false, 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/utils/validators.ts: -------------------------------------------------------------------------------- 1 | export function isNotBlank(str: string | undefined | null): str is string { 2 | return !!(str && str.trim() !== ''); 3 | } 4 | 5 | export function isNumeric(str: string) { 6 | if (typeof str !== 'string') return false; // we only process strings! 7 | return !isNaN(Number(str)) && !isNaN(parseFloat(str)); // ...and ensure strings of whitespace fail 8 | } 9 | 10 | export function isBlank(str: string | undefined | null): str is '' { 11 | return !isNotBlank(str); 12 | } 13 | 14 | export function isValidUsername(name: string) { 15 | // check length 16 | if (name.length < 2 || name.length > 20) { 17 | return false; 18 | } 19 | // regular expression for username validation 20 | const regex = /^[^.][a-z0-9.]*[^.]$/i; 21 | // check invalid characters 22 | if (/[\&\*\?=_'"“‘,,+\-<>]/.test(name)) { 23 | return false; 24 | } 25 | // check consecutive periods 26 | if (/\.{2,}/.test(name)) { 27 | return false; 28 | } 29 | // check against regular expression 30 | if (!regex.test(name)) { 31 | return false; 32 | } 33 | return true; 34 | } 35 | 36 | export function isValidEmail(email: string) { 37 | const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; 38 | return pattern.test(email); 39 | } 40 | 41 | export function isValidPassword(password: string) { 42 | return ( 43 | password.length >= 6 && 44 | password.length <= 20 && 45 | /\d/.test(password) && 46 | /[a-zA-Z]/.test(password) 47 | ); 48 | } 49 | 50 | export function isValidHttpHRL(url: string) { 51 | const pattern = /^(http|https):\/\/[^ "]+$/; 52 | return pattern.test(url); 53 | } 54 | 55 | // containing only letters (can be both cases) and numbers or hyphen, and can't start with a digit, and can't end with a hyphen 56 | export function isValidMCPServerKey(key: string) { 57 | return /^[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$/.test(key) && !/-{2,}/.test(key); 58 | } 59 | 60 | 61 | export function isValidMCPServer(server: any): boolean { 62 | if (!server || typeof server !== 'object') return false; 63 | if (!server.name || typeof server.name !== 'string') return false; 64 | const hasUrl = typeof server.url === 'string'; 65 | const hasCmd = typeof server.command === 'string'; 66 | if (!hasUrl && !hasCmd) return false; 67 | if (hasUrl && hasCmd) return false; 68 | if (server.args && !Array.isArray(server.args)) return false; 69 | if (server.headers && typeof server.headers !== 'object') return false; 70 | if (server.env && typeof server.env !== 'object') return false; 71 | if (server.description && typeof server.description !== 'string') return false; 72 | return true; 73 | } 74 | -------------------------------------------------------------------------------- /src/vendors/axiom.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { Axiom } from '@axiomhq/js'; 3 | import { captureException } from '../main/logging'; 4 | 5 | let axiom: any = null; 6 | 7 | function getAxiom() { 8 | if (!axiom) { 9 | try { 10 | axiom = new Axiom({ 11 | token: process.env.AXIOM_TOKEN as string, 12 | orgId: process.env.AXIOM_ORG_ID as string, 13 | }); 14 | } catch (err: any) { 15 | captureException(err); 16 | } 17 | } 18 | return axiom; 19 | } 20 | 21 | export default { 22 | ingest(data: { [key: string]: any }[]) { 23 | try { 24 | const axiom = getAxiom(); 25 | axiom && axiom.ingest('5ire', data); 26 | } catch (err: any) { 27 | captureException(err); 28 | } 29 | }, 30 | async flush() { 31 | try { 32 | const axiom = getAxiom(); 33 | axiom && (await axiom.flush()); 34 | } catch (err: any) { 35 | captureException(err); 36 | } 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/vendors/supa.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | import { captureException } from '../renderer/logging'; 3 | 4 | const supabase = createClient( 5 | `https://${window.envVars.SUPA_PROJECT_ID}.supabase.co`, 6 | window.envVars.SUPA_KEY as string, 7 | ); 8 | 9 | export async function fetchById( 10 | table: string, 11 | id: number, 12 | columns: string = '*', 13 | ): Promise { 14 | const { data, error } = await supabase 15 | .from(table) 16 | .select(columns) 17 | .eq('id', id) 18 | .maybeSingle(); 19 | if (error) { 20 | captureException(new Error(error.message)); 21 | throw error; 22 | } 23 | return data as Type; 24 | } 25 | 26 | export default supabase; 27 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/!(node_modules)/**/*.{jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | darkMode: "class" 9 | }; 10 | -------------------------------------------------------------------------------- /test/assets/AI-Career.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/test/assets/AI-Career.pdf -------------------------------------------------------------------------------- /test/assets/SOTA.md: -------------------------------------------------------------------------------- 1 | SOTA是“State of the Art”的缩写,通常指在某一特定领域或任务中表现最好的方法或模型。这个术语广泛应用于科技和工程领域,尤其是在深度学习、计算机视觉、软件开发等领域。在这些领域中,SOTA代表了当前技术的最高水平,是该领域内公认的最先进的解决方案 2 | 3 | SOTA模型或结果不是指某一个具体的模型或结果,而是指在特定的基准测试或研究任务中,目前性能最优的模型或结果。这种表述方式强调了持续进步和追求卓越的重要性,同时也激励着科研人员和工程师不断探索新的可能性,以超越现有的技术边界。 4 | 5 | 此外,值得注意的是,SOTA并非静态不变,它会随着时间和新技术的出现而更新。因此,了解和追踪SOTA对于保持技术领先地位至关重要 -------------------------------------------------------------------------------- /test/assets/出师表.txt: -------------------------------------------------------------------------------- 1 | 出师表 2 | 【三国】诸葛亮 3 | 4 | 先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。 5 | 6 | 宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。 7 | 8 | 侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必能裨补阙漏,有所广益。 9 | 10 | 将军向宠,性行淑均,晓畅军事,试用于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。 11 | 12 | 亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。 13 | 14 | 臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。 15 | 16 | 先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐托付不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。 17 | 18 | 愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。 19 | 20 | 今当远离,临表涕零,不知所言。 -------------------------------------------------------------------------------- /test/assets/探索智慧的疆界.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/test/assets/探索智慧的疆界.pptx -------------------------------------------------------------------------------- /test/assets/演示项目.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/test/assets/演示项目.xlsx -------------------------------------------------------------------------------- /test/assets/长恨歌.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/test/assets/长恨歌.docx -------------------------------------------------------------------------------- /test/data/05-versions-space.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/5ire/48c1ae308d160087d59458eb264f4c82dbb6856e/test/data/05-versions-space.pdf -------------------------------------------------------------------------------- /test/intellichat/validators.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals'; 2 | import { 3 | isValidMaxTokens, 4 | isValidTemperature, 5 | } from '../../src/intellichat/validators'; 6 | 7 | describe('intellichat/validators', () => { 8 | test('isValidMaxToken', () => { 9 | const maxToken1 = 4096; 10 | expect(isValidMaxTokens(maxToken1, 'OpenAI', 'gpt-4-1106-preview')).toBe( 11 | true, 12 | ); 13 | expect(isValidMaxTokens(maxToken1, 'OpenAI', 'gpt-4-vision-preview')).toBe( 14 | true, 15 | ); 16 | expect(isValidMaxTokens(maxToken1, 'OpenAI', 'gpt-4-0613')).toBe(true); 17 | expect(isValidMaxTokens(maxToken1, 'Baidu', 'ERNIE-Bot 4.0')).toBe(true); 18 | 19 | const maxToken2 = 32768; 20 | expect(isValidMaxTokens(maxToken1, 'OpenAI', 'gpt-4-1106-preview')).toBe( 21 | true, 22 | ); 23 | expect(isValidMaxTokens(maxToken2, 'OpenAI', 'gpt-4-32k-0613')).toBe(false); 24 | expect(isValidMaxTokens(maxToken2, 'OpenAI', 'gpt-4-0613')).toBe(false); 25 | expect(isValidMaxTokens(maxToken2, 'Baidu', 'ERNIE-Bot')).toBe(false); 26 | 27 | expect(isValidMaxTokens(maxToken2, 'OpenAI', 'ERNIE-Bot 4.0')).toBe(false); 28 | }); 29 | 30 | test('isValidTemperature', () => { 31 | expect(isValidTemperature(1, 'OpenAI')).toBe(true); 32 | expect(isValidTemperature(1, 'Baidu')).toBe(true); 33 | expect(isValidTemperature(0, 'OpenAI')).toBe(true); 34 | expect(isValidTemperature(0, 'Baidu')).toBe(false); 35 | 36 | expect(isValidTemperature(2, 'OpenAI')).toBe(true); 37 | expect(isValidTemperature(2, 'Baidu')).toBe(false); 38 | 39 | expect(isValidTemperature(-1, 'OpenAI')).toBe(false); 40 | expect(isValidTemperature(-1, 'Baidu')).toBe(false); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/main/docloader.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from '@jest/globals'; 2 | import { loadDocument } from '../../src/main/docloader'; 3 | import path from 'path'; 4 | import { getFileType } from '../../src/main/util'; 5 | 6 | describe('DocLader', () => { 7 | it('GetFileType', async () => { 8 | const file1 = path.join(__dirname, '../assets/AI-Career.pdf'); 9 | expect(await getFileType(file1)).toBe('pdf'); 10 | const file2 = path.join(__dirname, '../assets/演示项目.xlsx'); 11 | expect(await getFileType(file2)).toBe('xlsx'); 12 | const file3 = path.join(__dirname, '../assets/长恨歌.docx'); 13 | expect(await getFileType(file3)).toBe('docx'); 14 | const file4 = path.join(__dirname, '../assets/出师表.txt'); 15 | expect(await getFileType(file4)).toBe('txt'); 16 | const file5 = path.join(__dirname, '../assets/SOTA.md'); 17 | expect(await getFileType(file5)).toBe('md'); 18 | }); 19 | 20 | it('Load PDF file', async () => { 21 | const file = path.join(__dirname, '../assets/AI-Career.pdf'); 22 | const content = await loadDocument(file, 'pdf'); 23 | expect(content).toContain('Coding AI is the New Literacy'); 24 | }); 25 | 26 | it('Load XLSX file', async () => { 27 | const file = path.join(__dirname, '../assets/演示项目.xlsx'); 28 | const content = await loadDocument(file, 'xlsx'); 29 | expect(content).toContain('关于成立某某县监察委员会驻某某乡监察室的请示'); 30 | expect(content).toContain('某某县委党建工作领导小组办公室'); 31 | expect(content).toContain('20180915'); 32 | }); 33 | 34 | it('Load DOCX file', async () => { 35 | const file = path.join(__dirname, '../assets/长恨歌.docx'); 36 | const content = await loadDocument(file, 'docx'); 37 | expect(content).toContain('汉皇重色思倾国'); 38 | expect(content).toContain('御宇多年求不得'); 39 | expect(content).toContain('【唐】白居易'); 40 | }); 41 | 42 | it('Load PPTX file', async () => { 43 | const file = path.join(__dirname, '../assets/探索智慧的疆界.pptx'); 44 | const content = (await loadDocument(file, 'pptx')).replace(/\s+/g, ''); 45 | expect(content).toContain('探索智慧的疆界'); 46 | expect(content).toContain('AGISurge'); 47 | expect(content).toContain('标志着人类正式迈入了人工智能时代'); 48 | }); 49 | 50 | it('Load TXT file', async () => { 51 | const file = path.join(__dirname, '../assets/出师表.txt'); 52 | const content = await loadDocument(file, 'txt'); 53 | expect(content).toContain('【三国】诸葛亮'); 54 | expect(content).toContain('先帝创业未半而中道崩殂'); 55 | expect(content).toContain('此臣所以报先帝而忠陛下之职分也'); 56 | 57 | const file1 = path.join(__dirname, '../assets/SOTA.md'); 58 | const content1 = await loadDocument(file1, 'md'); 59 | expect(content1).toContain('SOTA'); 60 | expect(content1).toContain('计算机视觉'); 61 | expect(content1).toContain('它会随着时间和新技术的出现而更新'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/main/embedder.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe } from '@jest/globals'; 2 | import { embed } from '../../src/main/embedder'; 3 | 4 | 5 | 6 | beforeAll(() => { 7 | 8 | const originalImplementation = Array.isArray; 9 | // @ts-ignore 10 | Array.isArray = jest.fn((type) => { 11 | if (type && type.constructor && (type.constructor.name === "Float32Array" || type.constructor.name === "BigInt64Array")) { 12 | return true; 13 | } 14 | return originalImplementation(type); 15 | }); 16 | }); 17 | 18 | describe('Embedder', () => { 19 | it('embed', async () => { 20 | const progressCallback = (total:number,done:number) => { 21 | console.log(`Progress: ${done}/${total}`); 22 | }; 23 | const texts = ['杨家有女初长成', '养在深闺人未识']; 24 | const result = await embed(texts, progressCallback); 25 | expect(result.length).toBe(2); 26 | }); 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /test/main/knowledge.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from '@jest/globals'; 2 | import Knowledge from '../../src/main/knowledge'; 3 | import { embed } from '../../src/main/embedder'; 4 | import { randomId } from '../../src/main/util'; 5 | 6 | beforeAll(async () => { 7 | const originalImplementation = Array.isArray; 8 | // @ts-ignore 9 | Array.isArray = jest.fn((type) => { 10 | if ( 11 | type && 12 | type.constructor && 13 | (type.constructor.name === 'Float32Array' || 14 | type.constructor.name === 'BigInt64Array') 15 | ) { 16 | return true; 17 | } 18 | return originalImplementation(type); 19 | }); 20 | }); 21 | 22 | describe('VectorDB', () => { 23 | it('getInstance', async () => { 24 | const db = await Knowledge.getDatabase(); 25 | expect(db).toBeDefined(); 26 | }); 27 | 28 | it('Add', async () => { 29 | const texts = [ 30 | `三月七日,沙湖道中遇雨。雨具先去,同行皆狼狈,余独不觉。已而遂晴,故作此词。 31 | 莫听穿林打叶声,何妨吟啸且徐行。竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。 32 | 料峭春风吹酒醒,微冷,山头斜照却相迎。回首向来萧瑟处,归去,也无风雨也无晴`, 33 | `基于即将启动的“未来城市大奖2024”,36氪将与清华大学人工智能国际治理研究院等机构密切沟通合作,通过场景征集、案例互荐、成果联合发布等形式,共同助力《白皮书》的编著及推广。`, 34 | `今年1月2日,蜜雪冰城、古茗同日向港交所递交上市招股书,然而它们的上市进程一直没有实质性进展,如今招股书均已失效。值得注意的是,招股书失效并不意味着企业放弃上市,它们可以补充新的财务数据,再次递交。但截至发稿,均未在港交所发现两家企业重新递表。`, 35 | `7 月 2 日凌晨,知名人工智能专家、OpenAI 的联合创始人 Andrej Karpathy 在社交平台上发帖,提出了一个关于未来计算机的构想:“100% Fully Software2.0”, 计算机未来将完全由神经网络驱动,不依赖传统软件代码。`, 36 | ]; 37 | const vector: any = await embed(texts); 38 | const data = texts.map((text, index) => { 39 | return { 40 | id: randomId(), 41 | collection_id: '1', 42 | file_id: '1', 43 | content: text, 44 | vector: vector[index], 45 | }; 46 | }); 47 | await Knowledge.add(data); 48 | }); 49 | 50 | it('Search', async () => { 51 | const texts1 = [`何妨吟啸且徐行`]; 52 | const vector1: any = await embed(texts1); 53 | const result1 = await Knowledge.search(['1'], vector1[0], { limit: 1 }); 54 | expect(result1[0].content).toContain('一蓑烟雨任平生'); 55 | 56 | const texts2 = [`软件未来将由神经网络驱动`]; 57 | const vector2: any = await embed(texts2); 58 | const result2 = await Knowledge.search(['1'], vector2[0], { limit: 1 }); 59 | expect(result2[0].content).toContain('Andrej Karpathy'); 60 | }); 61 | }); 62 | 63 | afterAll(async () => { 64 | await Knowledge.remove({ collectionId: 1 }); 65 | await Knowledge.close(); 66 | }); 67 | -------------------------------------------------------------------------------- /test/main/util.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { describe, expect, test } from '@jest/globals'; 3 | import { slidingWindowChunk } from '../../src/main/util'; 4 | 5 | describe('main/util', () => { 6 | test('slideWindowChunk', () => { 7 | const text = ` 8 | 将进酒·君不见 9 | 唐·李白 10 | 11 | 君不见,黄河之水天上来,奔流到海不复回。 12 | 君不见,高堂明镜悲白发,朝如青丝暮成雪。 13 | 人生得意须尽欢,莫使金樽空对月。 14 | 天生我材必有用,千金散尽还复来。 15 | 烹羊宰牛且为乐,会须一饮三百杯。 16 | 岑夫子,丹丘生,将进酒,杯莫停。 17 | 与君歌一曲,请君为我倾耳听。 18 | 钟鼓馔玉不足贵,但愿长醉不愿醒。 19 | 古来圣贤皆寂寞,惟有饮者留其名。 20 | 陈王昔时宴平乐,斗酒十千恣欢谑。 21 | 主人何为言少钱,径须沽取对君酌。 22 | 五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。 23 | ` 24 | const result = slidingWindowChunk(text.replace(/\s+/g,'')) 25 | expect(result.length).toBe(1) 26 | }); 27 | }) 28 | -------------------------------------------------------------------------------- /test/mocks/electron-log.js: -------------------------------------------------------------------------------- 1 | export default { 2 | error: (msg)=>console.error(msg), 3 | debug: (msg)=>console.debug(msg), 4 | info: (msg)=>console.info(msg), 5 | warn: (msg)=>console.warn(msg), 6 | log: (msg)=>console.log(msg) 7 | }; 8 | -------------------------------------------------------------------------------- /test/mocks/electron.js: -------------------------------------------------------------------------------- 1 | export const app = { 2 | getPath: jest.fn().mockReturnValue('/Users/ironben/Library/Application Support/5ire'), 3 | quit: jest.fn(), 4 | }; 5 | -------------------------------------------------------------------------------- /test/utils/token.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals'; 2 | import { IChatRequestMessage } from '../../src/intellichat/types'; 3 | import { countGPTTokens } from '../../src/utils/token'; 4 | 5 | describe('utils/token', () => { 6 | test('countGPTTokens', () => { 7 | const exampleMessages = [ 8 | { 9 | role: 'system', 10 | content: 11 | 'You are a helpful, pattern-following assistant that translates corporate jargon into plain English.', 12 | }, 13 | { 14 | role: 'system', 15 | name: 'example_user', 16 | content: 'New synergies will help drive top-line growth.', 17 | }, 18 | { 19 | role: 'system', 20 | name: 'example_assistant', 21 | content: 'Things working well together will increase revenue.', 22 | }, 23 | { 24 | role: 'system', 25 | name: 'example_user', 26 | content: 27 | "Let's circle back when we have more bandwidth to touch base on opportunities for increased leverage.", 28 | }, 29 | { 30 | role: 'system', 31 | name: 'example_assistant', 32 | content: 33 | "Let's talk later when we're less busy about how to do better.", 34 | }, 35 | { 36 | role: 'user', 37 | content: 38 | "This late pivot means we don't have time to boil the ocean for the client deliverable.", 39 | }, 40 | ] as IChatRequestMessage[]; 41 | const numTokens = countGPTTokens(exampleMessages, 'gpt-4'); 42 | expect(numTokens).toBe(129); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2021", 5 | "module": "NodeNext", 6 | "lib": ["dom", "es2021"], 7 | "jsx": "react-jsx", 8 | "strict": true, 9 | "sourceMap": true, 10 | "baseUrl": "./src", 11 | "moduleResolution": "NodeNext", 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "resolveJsonModule": true, 15 | "allowJs": true, 16 | "outDir": ".erb/dll" 17 | }, 18 | "include": ["src", ".erb/scripts", ".eslintrc.js"], 19 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"] 20 | } 21 | --------------------------------------------------------------------------------