├── src ├── d.ts ├── types │ ├── d.ts │ ├── appearance.d.ts │ ├── device.d.ts │ ├── error.d.ts │ ├── auth.d.ts │ ├── bookmark.d.ts │ ├── usage.d.ts │ ├── settings.d.ts │ └── knowledge.d.ts ├── assets │ └── images │ │ ├── door.dark.png │ │ ├── hint.dark.png │ │ ├── design.dark.png │ │ ├── design.light.png │ │ ├── door.light.png │ │ ├── hint.light.png │ │ ├── reading.dark.png │ │ ├── tools.dark.png │ │ ├── tools.light.png │ │ ├── usage.dark.png │ │ ├── usage.light.png │ │ ├── knowledge.dark.png │ │ ├── reading.light.png │ │ ├── construction.dark.png │ │ ├── knowledge.light.png │ │ └── construction.light.png ├── renderer │ ├── apps │ │ ├── sora │ │ │ ├── app-sora.png │ │ │ ├── App.tsx │ │ │ └── index.tsx │ │ ├── leonardo │ │ │ ├── App.tsx │ │ │ ├── app-leonardo.png │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── types.ts │ │ ├── NotFound.tsx │ │ └── Loader.tsx │ ├── components │ │ ├── layout │ │ │ ├── AppHeader.scss │ │ │ ├── aside │ │ │ │ ├── AppNav.tsx │ │ │ │ ├── AppSidebar.scss │ │ │ │ ├── AppSidebar.tsx │ │ │ │ ├── ChatNav.tsx │ │ │ │ └── BookmarkNav.tsx │ │ │ └── AppHeader.tsx │ │ ├── ToolSpinner.scss │ │ ├── TooltipIcon.tsx │ │ ├── Spinner.tsx │ │ ├── StateButton.tsx │ │ ├── Spinner.scss │ │ ├── MaskableInput.tsx │ │ ├── ToolSpinner.tsx │ │ ├── Assets.tsx │ │ ├── StateInput.tsx │ │ ├── MaskableStateInput.tsx │ │ ├── Empty.tsx │ │ ├── ToolStatusIndicator.tsx │ │ ├── ConfirmDialog.tsx │ │ └── AlertDialog.tsx │ ├── preload.d.ts │ ├── pages │ │ ├── bookmark │ │ │ └── Bookmark.scss │ │ ├── settings │ │ │ ├── Settings.scss │ │ │ ├── AppearanceSettings.tsx │ │ │ └── Version.tsx │ │ ├── chat │ │ │ ├── Messages.tsx │ │ │ ├── Editor │ │ │ │ └── Toolbar │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── StreamCtrl.tsx │ │ │ ├── CitationDialog.tsx │ │ │ ├── Header.tsx │ │ │ └── Chat.scss │ │ ├── apps │ │ │ └── index.tsx │ │ ├── knowledge │ │ │ └── index.tsx │ │ ├── prompt │ │ │ └── index.tsx │ │ ├── user │ │ │ └── TabPassword.tsx │ │ └── tool │ │ │ ├── index.tsx │ │ │ └── ParamsDialog.tsx │ ├── index.tsx │ ├── i18n.ts │ ├── fluentui.scss │ ├── logging.ts │ ├── index.ejs │ ├── variables.scss │ └── App.tsx ├── hooks │ ├── useNav.ts │ ├── useLazyEffect.ts │ ├── useChatService.ts │ ├── useOnlineStatus.ts │ ├── useToast.tsx │ ├── useMarkdown.ts │ └── useProvider.ts ├── CrashReporter.ts ├── intellichat │ ├── readers │ │ ├── IChatReader.ts │ │ ├── OllamaChatReader.ts │ │ ├── OpenAIReader.ts │ │ ├── FireReader.ts │ │ ├── ChatBroReader.ts │ │ └── AnthropicReader.ts │ ├── services │ │ ├── IChatService.ts │ │ ├── INextCharService.ts │ │ ├── GrokChatService.ts │ │ ├── DeepSeekChatService.ts │ │ ├── DoubaoChatService.ts │ │ ├── AzureChatService.ts │ │ ├── MoonshotChatService.ts │ │ ├── OllamaChatService.ts │ │ ├── FireChatService.ts │ │ ├── index.ts │ │ └── ChatBroChatService.ts │ └── validators.ts ├── consts.ts ├── vendors │ ├── axiom.ts │ └── supa.ts ├── providers │ ├── DeepSeek.ts │ ├── Grok.ts │ ├── Ollama.ts │ ├── Moonshot.ts │ ├── index.ts │ ├── Fire.ts │ ├── ChatBro.ts │ ├── Google.ts │ ├── Doubao.ts │ └── types.ts ├── utils │ ├── cache.ts │ ├── validators.ts │ ├── mcp.ts │ └── token.ts ├── main │ ├── logging.ts │ ├── crypt.ts │ ├── docloader.ts │ └── downloader.ts └── stores │ ├── useStageStore.ts │ ├── useAppearanceStore.ts │ ├── useSettingsStore.ts │ └── useUsageStore.ts ├── .erb ├── mocks │ └── fileMock.js ├── img │ └── erb-logo.png ├── configs │ ├── .eslintrc │ ├── webpack.config.eslint.ts │ ├── webpack.paths.ts │ ├── webpack.config.renderer.dev.dll.ts │ ├── webpack.config.base.ts │ ├── webpack.config.preload.dev.ts │ └── webpack.config.main.prod.ts └── scripts │ ├── .eslintrc │ ├── link-modules.ts │ ├── clean.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── delete-source-maps.js │ ├── electron-rebuild.js │ ├── remove-locales.js │ ├── check-build-exists.ts │ ├── notarize.js │ ├── remove-useless.js │ └── check-native-dep.js ├── assets ├── icon.icns ├── icon.ico ├── icon.png ├── dockicon.png ├── icons │ ├── 128x128.png │ ├── 16x16.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 512x512.png │ ├── 64x64.png │ └── 1024x1024.png ├── fonts │ ├── barlow400.woff2 │ └── JetBrainsMono.woff2 ├── entitlements.mac.plist └── assets.d.ts ├── test ├── assets │ ├── 演示项目.xlsx │ ├── 长恨歌.docx │ ├── 探索智慧的疆界.pptx │ ├── AI-Career.pdf │ ├── SOTA.md │ └── 出师表.txt ├── mocks │ ├── electron.js │ └── electron-log.js ├── main │ ├── util.spec.ts │ ├── embedder.spec.ts │ ├── knowledge.spec.ts │ └── docloader.spec.ts ├── utils │ ├── token.test.ts │ ├── mcp.test.ts │ └── validators.test.ts └── intellichat │ └── validators.spec.ts ├── tailwind.config.js ├── postcss.config.js ├── .editorconfig ├── .gitattributes ├── .vscode-upload.json ├── .gitignore ├── tsconfig.json ├── .eslintignore ├── installer.nsh ├── release └── app │ └── package.json └── .eslintrc.js /src/d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; 2 | -------------------------------------------------------------------------------- /.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /src/types/d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | Canny: any; 3 | } 4 | -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/icon.png -------------------------------------------------------------------------------- /src/types/appearance.d.ts: -------------------------------------------------------------------------------- 1 | export type ThemeType = 'light' | 'dark' | 'system'; 2 | -------------------------------------------------------------------------------- /assets/dockicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/dockicon.png -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /test/assets/演示项目.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/test/assets/演示项目.xlsx -------------------------------------------------------------------------------- /test/assets/长恨歌.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/test/assets/长恨歌.docx -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/icons/64x64.png -------------------------------------------------------------------------------- /test/assets/探索智慧的疆界.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/test/assets/探索智慧的疆界.pptx -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /test/assets/AI-Career.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/test/assets/AI-Career.pdf -------------------------------------------------------------------------------- /assets/fonts/barlow400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/fonts/barlow400.woff2 -------------------------------------------------------------------------------- /src/assets/images/door.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/door.dark.png -------------------------------------------------------------------------------- /src/assets/images/hint.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/hint.dark.png -------------------------------------------------------------------------------- /assets/fonts/JetBrainsMono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/assets/fonts/JetBrainsMono.woff2 -------------------------------------------------------------------------------- /src/assets/images/design.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/design.dark.png -------------------------------------------------------------------------------- /src/assets/images/design.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/design.light.png -------------------------------------------------------------------------------- /src/assets/images/door.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/door.light.png -------------------------------------------------------------------------------- /src/assets/images/hint.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/hint.light.png -------------------------------------------------------------------------------- /src/assets/images/reading.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/reading.dark.png -------------------------------------------------------------------------------- /src/assets/images/tools.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/tools.dark.png -------------------------------------------------------------------------------- /src/assets/images/tools.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/tools.light.png -------------------------------------------------------------------------------- /src/assets/images/usage.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/usage.dark.png -------------------------------------------------------------------------------- /src/assets/images/usage.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/usage.light.png -------------------------------------------------------------------------------- /src/assets/images/knowledge.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/knowledge.dark.png -------------------------------------------------------------------------------- /src/assets/images/reading.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/reading.light.png -------------------------------------------------------------------------------- /src/renderer/apps/sora/app-sora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/renderer/apps/sora/app-sora.png -------------------------------------------------------------------------------- /src/assets/images/construction.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/construction.dark.png -------------------------------------------------------------------------------- /src/assets/images/knowledge.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/knowledge.light.png -------------------------------------------------------------------------------- /src/assets/images/construction.light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/5ire/main/src/assets/images/construction.light.png -------------------------------------------------------------------------------- /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/kuilenren/5ire/main/src/renderer/apps/leonardo/app-leonardo.png -------------------------------------------------------------------------------- /src/renderer/apps/sora/App.tsx: -------------------------------------------------------------------------------- 1 | // unused component 2 | export default function Leonardo() { 3 | return
Leonardo
4 | } 5 | -------------------------------------------------------------------------------- /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/renderer/apps/index.ts: -------------------------------------------------------------------------------- 1 | // unused component 2 | import leonardo from './leonardo'; 3 | import sora from './sora' 4 | 5 | export default [leonardo, sora]; 6 | -------------------------------------------------------------------------------- /.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/components/layout/AppHeader.scss: -------------------------------------------------------------------------------- 1 | 2 | .app-header{ 3 | position: fixed; 4 | left:0; 5 | top:0; 6 | z-index: 1; 7 | -webkit-app-region: drag; 8 | } 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | }; 9 | -------------------------------------------------------------------------------- /src/renderer/components/layout/aside/AppNav.tsx: -------------------------------------------------------------------------------- 1 | export default function AppNav({ collapsed }: { collapsed: boolean }) { 2 | return ( 3 |
4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/renderer/apps/sora/index.tsx: -------------------------------------------------------------------------------- 1 | import icon from './app-sora.png' 2 | 3 | export default { 4 | name: 'Sora', 5 | description: 6 | 'Sora generates videos from natural language descriptions', 7 | isEnabled: false, 8 | key: 'sora', 9 | icon 10 | }; 11 | 12 | 13 | -------------------------------------------------------------------------------- /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/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 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /src/renderer/apps/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | import Empty from '../../renderer/components/Empty'; 3 | 4 | export default function NotFound() { 5 | const { t } = useTranslation(); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/apps/leonardo/index.tsx: -------------------------------------------------------------------------------- 1 | import icon from './app-leonardo.png' 2 | 3 | export default { 4 | name: 'Leonardo', 5 | description: 6 | 'Leonardo generates images from natural language descriptions', 7 | isEnabled: false, 8 | key: 'leonardo', 9 | icon 10 | }; 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/types/bookmark.d.ts: -------------------------------------------------------------------------------- 1 | export interface IBookmark { 2 | id: string; 3 | msgId: string; 4 | prompt: string; 5 | reply: string; 6 | model: string; 7 | temperature: number; 8 | memo?: string; 9 | favorite?: boolean; 10 | citedFiles?: string; 11 | citedChunks?: string; 12 | createdAt: number; 13 | } 14 | -------------------------------------------------------------------------------- /test/assets/SOTA.md: -------------------------------------------------------------------------------- 1 | SOTA是“State of the Art”的缩写,通常指在某一特定领域或任务中表现最好的方法或模型。这个术语广泛应用于科技和工程领域,尤其是在深度学习、计算机视觉、软件开发等领域。在这些领域中,SOTA代表了当前技术的最高水平,是该领域内公认的最先进的解决方案 2 | 3 | SOTA模型或结果不是指某一个具体的模型或结果,而是指在特定的基准测试或研究任务中,目前性能最优的模型或结果。这种表述方式强调了持续进步和追求卓越的重要性,同时也激励着科研人员和工程师不断探索新的可能性,以超越现有的技术边界。 4 | 5 | 此外,值得注意的是,SOTA并非静态不变,它会随着时间和新技术的出现而更新。因此,了解和追踪SOTA对于保持技术领先地位至关重要 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-24; 14 | @apply font-semibold; 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/pages/chat/Messages.tsx: -------------------------------------------------------------------------------- 1 | import Message from './Message'; 2 | import { IChatMessage } from 'intellichat/types'; 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | }] -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/renderer/components/layout/aside/AppSidebar.scss: -------------------------------------------------------------------------------- 1 | .app-sidebar{ 2 | height:100vh; 3 | --tw-bg-opacity: 0.9; 4 | --tw-border-opacity:1; 5 | background-color: rgba(var(--color-bg-sidebar),var(--tw-bg-opacity)); 6 | border-color: rgba(var(--color-border),var(--tw-border-opacity)); 7 | } 8 | 9 | .Canny_BadgeContainer .Canny_Badge { 10 | position: absolute; 11 | top: 5px; 12 | right: 5px; 13 | border-radius: 10px; 14 | background-color: red; 15 | padding: 5px; 16 | border: 1px solid white; 17 | } 18 | -------------------------------------------------------------------------------- /src/types/usage.d.ts: -------------------------------------------------------------------------------- 1 | import { ProviderType } from '../providers/types'; 2 | 3 | export interface IUsage { 4 | id: string; 5 | provider: ProviderType; 6 | model: string; 7 | inputTokens: number; 8 | outputTokens: number; 9 | inputPrice: number; 10 | outputPrice: number; 11 | createdAt: number; 12 | } 13 | 14 | export interface IUsageStatistics { 15 | provider: ProviderType; 16 | model: string; 17 | inputTokens: number; 18 | outputTokens: number; 19 | inputCost: number; 20 | outputCost: number; 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/components/ToolSpinner.scss: -------------------------------------------------------------------------------- 1 | .spinner_mHwL { 2 | animation: spinner_1 0.75s cubic-bezier(0.56, 0.52, 0.17, 0.98) infinite; 3 | } 4 | .spinner_ote2 { 5 | animation: spinner_2 0.75s cubic-bezier(0.56, 0.52, 0.17, 0.98) infinite; 6 | } 7 | @keyframes spinner_1 { 8 | 0% { 9 | cx: 4px; 10 | r: 3px; 11 | } 12 | 50% { 13 | cx: 9px; 14 | r: 8px; 15 | } 16 | } 17 | @keyframes spinner_2 { 18 | 0% { 19 | cx: 15px; 20 | r: 8px; 21 | } 22 | 50% { 23 | cx: 20px; 24 | r: 3px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/types/settings.d.ts: -------------------------------------------------------------------------------- 1 | import { ProviderType } from 'providers/types'; 2 | import { ThemeType } from './appearance'; 3 | 4 | 5 | export interface IAPISettings { 6 | provider: ProviderType; 7 | base: string; 8 | key: string; 9 | model: string; 10 | secret?: string; 11 | deploymentId?: string; 12 | endpoint?: string; 13 | } 14 | 15 | export interface ISettings { 16 | theme: ThemeType; 17 | api: { 18 | activeProvider: string; 19 | providers: { 20 | [key: string]: IAPISettings; 21 | }; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/hooks/useChatService.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import useSettingsStore from 'stores/useSettingsStore'; 3 | import useChatContext from './useChatContext'; 4 | import createService from '../intellichat/services'; 5 | import INextChatService from 'intellichat/services/INextCharService'; 6 | 7 | const debug = Debug('5ire:hooks:useService'); 8 | 9 | export default function useChatService():INextChatService { 10 | const context = useChatContext(); 11 | const { provider } = useSettingsStore.getState().api; 12 | return createService(provider, context); 13 | } 14 | -------------------------------------------------------------------------------- /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 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"] 19 | } 20 | -------------------------------------------------------------------------------- /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 | tool?: ITool | null; 10 | inputTokens?: number; 11 | outputTokens?: number; 12 | } 13 | export default interface IChatReader { 14 | read({ 15 | onError, 16 | onProgress, 17 | onToolCalls, 18 | }: { 19 | onError: (error: any) => void; 20 | onProgress: (chunk: string) => void; 21 | onToolCalls: (toolCalls: any) => void; 22 | }): Promise; 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/components/TooltipIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from '@fluentui/react-components'; 2 | import { Info16Regular } from '@fluentui/react-icons'; 3 | 4 | export default function TooltipIcon({ 5 | tip, 6 | }: { 7 | tip: string | undefined | null; 8 | }) { 9 | return tip ? ( 10 | 18 | 19 | 20 | ) : null; 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import './Spinner.scss'; 2 | 3 | export default function Spinner({ 4 | size=24, 5 | className='', 6 | }: { 7 | size?: number; 8 | className?: string; 9 | }) { 10 | return ( 11 |
15 |
16 |
17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/renderer/components/StateButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Spinner } from '@fluentui/react-components'; 2 | import { forwardRef } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | const StateButton = (props: { loading: boolean } & any, ref:any) => { 6 | const { t } = useTranslation(); 7 | const { loading, icon, ...rest } = props; 8 | return ( 9 | 16 | ); 17 | } 18 | 19 | export default forwardRef(StateButton); 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/renderer/components/Spinner.scss: -------------------------------------------------------------------------------- 1 | .spinner { 2 | display: inline-block; 3 | position: relative; 4 | } 5 | .spinner div { 6 | box-sizing: border-box; 7 | display: block; 8 | position: absolute; 9 | border: 3px solid #b4d6fa; 10 | border-radius: 50%; 11 | animation: spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 12 | border-color: #0f6cbd transparent transparent transparent; 13 | } 14 | .spinner div:nth-child(1) { 15 | animation-delay: -0.45s; 16 | border: 3px solid #b4d6fa; 17 | } 18 | .spinner div:nth-child(2) { 19 | animation-delay: -0.3s; 20 | } 21 | .spinner div:nth-child(3) { 22 | animation-delay: -0.15s; 23 | } 24 | @keyframes spinner { 25 | 0% { 26 | transform: rotate(0deg); 27 | } 28 | 100% { 29 | transform: rotate(360deg); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | 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 | chat(message:IChatRequestMessage[]):void; 15 | abort():void; 16 | isReady(): boolean; 17 | onComplete(callback: (result: any) => Promise): void; 18 | onToolCalls(callback: (toolName: string) => void): void; 19 | onReading(callback: (chunk: string) => void): void; 20 | onError(callback: (error: any, aborted: boolean) => void): void; 21 | } 22 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/assets/出师表.txt: -------------------------------------------------------------------------------- 1 | 出师表 2 | 【三国】诸葛亮 3 | 4 | 先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。 5 | 6 | 宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。 7 | 8 | 侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必能裨补阙漏,有所广益。 9 | 10 | 将军向宠,性行淑均,晓畅军事,试用于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。 11 | 12 | 亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。 13 | 14 | 臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。 15 | 16 | 先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐托付不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。 17 | 18 | 愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。 19 | 20 | 今当远离,临表涕零,不知所言。 -------------------------------------------------------------------------------- /src/intellichat/readers/OllamaChatReader.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { IChatResponseMessage } from 'intellichat/types'; 3 | import IChatReader, { ITool } 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.tool_calls, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5ire", 3 | "version": "0.9.0", 4 | "description": "An Open Source Cross-Platform LLMs Desktop Client", 5 | "license": "GPL-3.0-only", 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/consts.ts: -------------------------------------------------------------------------------- 1 | export const tempChatId = 'temp'; 2 | 3 | export const EARLIEST_DATE = new Date('2023-08-01'); 4 | 5 | export const NUM_CTX_MESSAGES = 3; 6 | export const MAX_CTX_MESSAGES = 10; 7 | export const MIN_CTX_MESSAGES = 0; 8 | 9 | export const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB 10 | export const SUPPORTED_FILE_TYPES: { [key: string]: string } = { 11 | txt: 'text/plain', 12 | md: 'text/plain', 13 | csv: 'text/csv', 14 | epub: 'application/epub+zip', 15 | docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 16 | pdf: 'application/pdf', 17 | xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 18 | pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 19 | }; 20 | export const SUPPORTED_IMAGE_TYPES: { [key: string]: string } = { 21 | jpg: 'image/jpeg', 22 | jpeg: 'image/jpeg', 23 | png: 'image/png', 24 | }; 25 | -------------------------------------------------------------------------------- /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/providers/DeepSeek.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | export default { 4 | name: 'DeepSeek', 5 | apiBase: 'https://api.deepseek.com', 6 | currency: 'CNY', 7 | options: { 8 | apiBaseCustomizable: true, 9 | apiKeyCustomizable: true, 10 | }, 11 | chat: { 12 | apiSchema: ['base', 'key', 'model'], 13 | presencePenalty: { min: -2, max: 2, default: 0 }, 14 | topP: { min: 0, max: 1, default: 1 }, 15 | temperature: { min: 0, max: 2, default: 1 }, 16 | options: { 17 | modelCustomizable: false, 18 | }, 19 | models: { 20 | 'deepseek-chat': { 21 | name: 'deepseek-chat', 22 | contextWindow: 128000, 23 | maxTokens: 4096, 24 | inputPrice: 0.0001, 25 | outputPrice: 0.001, 26 | isDefault: true, 27 | description: ``, 28 | toolEnabled: true, 29 | group: 'DeepSeek', 30 | } 31 | }, 32 | }, 33 | } as IServiceProvider; 34 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | const MaskableInput = (props: any, ref:any) => { 6 | const [showRaw, setShowRaw] = useState(false); 7 | return ( 8 | } 16 | appearance="subtle" 17 | onClick={() => setShowRaw(false)} 18 | /> 19 | ) : ( 20 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/pages/chat/Editor/Toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Toolbar } from '@fluentui/react-components'; 2 | import useChatContext from 'hooks/useChatContext'; 3 | import ModelCtrl from './ModelCtrl'; 4 | import PromptCtrl from './PromptCtrl'; 5 | import TemperatureCtrl from './TemperatureCtrl'; 6 | import MaxTokensCtrl from './MaxTokensCtrl'; 7 | import ImgCtrl from './ImgCtrl'; 8 | import StreamCtrl from './StreamCtrl'; 9 | import KnowledgeCtrl from './KnowledgeCtrl'; 10 | 11 | export default function EditorToolbar({ 12 | onConfirm, 13 | }: { 14 | onConfirm: () => void; 15 | }) { 16 | const ctx = useChatContext(); 17 | const provider = ctx.getProvider(); 18 | const chat = ctx.getActiveChat(); 19 | 20 | return ( 21 |
22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {provider.chat.options.streamCustomizable && ( 35 | 36 | )} 37 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /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 choice = JSON.parse(chunk).choices[0]; 11 | return { 12 | content: choice.delta.content || '', 13 | isEnd: false, 14 | toolCalls: choice.delta.tool_calls, 15 | }; 16 | } 17 | 18 | protected parseTools(respMsg: IChatResponseMessage): ITool | null { 19 | if (respMsg.toolCalls) { 20 | return { 21 | id: respMsg.toolCalls[0].id, 22 | name: respMsg.toolCalls[0].function.name, 23 | }; 24 | } 25 | return null; 26 | } 27 | 28 | protected parseToolArgs(respMsg: IChatResponseMessage): { 29 | index: number; 30 | args: string; 31 | } | null { 32 | try { 33 | if (respMsg.isEnd || !respMsg.toolCalls) { 34 | return null; 35 | } 36 | const toolCalls = respMsg.toolCalls[0]; 37 | return { 38 | index: toolCalls.index, 39 | args: toolCalls.function.arguments, 40 | }; 41 | } catch (err) { 42 | console.error('parseToolArgs', err); 43 | } 44 | return null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/providers/Moonshot.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | export default { 4 | name: 'Moonshot', 5 | apiBase: 'https://api.moonshot.cn', 6 | currency: 'CNY', 7 | options: { 8 | apiBaseCustomizable: false, 9 | apiKeyCustomizable: true 10 | }, 11 | chat: { 12 | apiSchema: ['base', 'key', 'model'], 13 | presencePenalty: { min: -2, max: 2, default: 0 }, 14 | topP: { min: 0, max: 1, default: 1 }, 15 | temperature: { min: 0, max: 1, default: 0.3 }, 16 | options: { 17 | modelCustomizable: true, 18 | }, 19 | models: { 20 | 'moonshot-v1-8k': { 21 | name: 'moonshot-v1-8k', 22 | contextWindow: 8192, 23 | maxTokens: 1024, 24 | inputPrice: 0.012, 25 | outputPrice: 0.012, 26 | isDefault: true, 27 | toolEnabled: true, 28 | group: 'Moonshot', 29 | }, 30 | 'moonshot-v1-32k': { 31 | name: 'moonshot-v1-32k', 32 | contextWindow: 32768, 33 | maxTokens: 1024, 34 | inputPrice: 0.024, 35 | outputPrice: 0.024, 36 | toolEnabled: true, 37 | group: 'Moonshot', 38 | }, 39 | 'moonshot-v1-128k': { 40 | name: 'moonshot-v1-128k', 41 | contextWindow: 131072, 42 | maxTokens: 1024, 43 | inputPrice: 0.06, 44 | outputPrice: 0.06, 45 | toolEnabled: true, 46 | group: 'Moonshot', 47 | }, 48 | }, 49 | }, 50 | } as IServiceProvider; 51 | -------------------------------------------------------------------------------- /src/intellichat/validators.ts: -------------------------------------------------------------------------------- 1 | import { captureException } from '../renderer/logging'; 2 | import { isNull, isNumber } from 'lodash'; 3 | import { isBlank } from 'utils/validators'; 4 | import { ProviderType } from '../providers/types'; 5 | import { getChatModel, getProvider } from '../providers'; 6 | 7 | const DEFAULT_MAX_TOKEN = 4096; 8 | 9 | export function isValidMaxTokens( 10 | maxTokens: number | null | undefined, 11 | providerName: ProviderType, 12 | modelName: string 13 | ): maxTokens is number | null { 14 | if (isNull(maxTokens)) return true; 15 | if (!isNumber(maxTokens)) return false; 16 | if (maxTokens <= 0) return false; 17 | 18 | const model = getChatModel(providerName, modelName); 19 | if (!model.name) { 20 | captureException( 21 | `Could find model:${modelName} for provider:${providerName}` 22 | ); 23 | } 24 | return maxTokens <= (model.maxTokens || DEFAULT_MAX_TOKEN); 25 | } 26 | 27 | export function isValidTemperature( 28 | temperature: number | null | undefined, 29 | providerName: ProviderType 30 | ): boolean { 31 | if (isBlank(providerName)) { 32 | return false; 33 | } 34 | if (!isNumber(temperature)) { 35 | return false; 36 | } 37 | const provider = getProvider(providerName); 38 | const { min, max, interval } = provider.chat.temperature; 39 | if (interval?.leftOpen ? temperature <= min : temperature < min) { 40 | return false; 41 | } 42 | if (interval?.rightOpen ? temperature >= max : temperature > max) { 43 | return false; 44 | } 45 | return true; 46 | } 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/renderer/pages/chat/CitationDialog.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 { Dismiss24Regular } from '@fluentui/react-icons'; 12 | import { useMemo } from 'react'; 13 | import { useTranslation } from 'react-i18next'; 14 | import useKnowledgeStore from 'stores/useKnowledgeStore'; 15 | 16 | export default function CitationDialog() { 17 | const { citation } = useKnowledgeStore(); 18 | 19 | const isOpen = useMemo(() => { 20 | return citation.open; 21 | }, [citation.open]); 22 | 23 | const close = () => { 24 | useKnowledgeStore.getState().hideCitation(); 25 | }; 26 | 27 | const { t } = useTranslation(); 28 | return ( 29 | 30 | 31 | 32 | 35 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/stores/useStageStore.ts: -------------------------------------------------------------------------------- 1 | import { produce } from 'immer'; 2 | import { IPrompt, IStage } from 'intellichat/types'; 3 | import { isNull, isUndefined, pickBy } from 'lodash'; 4 | import { create } from 'zustand'; 5 | 6 | export interface IStageStore { 7 | prompts: { [key: string]: IPrompt | null }; 8 | inputs: { [key: string]: string }; 9 | getPrompt: (chatId: string) => IPrompt | null; 10 | getInput: (chatId: string) => string; 11 | editStage: (chatId: string, stage: Partial) => void; 12 | deleteStage: (chatId: string) => void; 13 | } 14 | 15 | const useStageStore = create((set, get) => ({ 16 | prompts: {}, 17 | inputs: {}, 18 | getPrompt: (chatId: string) => { 19 | return get().prompts[chatId]; 20 | }, 21 | getInput: (chatId: string) => { 22 | return get().inputs[chatId]; 23 | }, 24 | editStage: (chatId: string, stage: Partial) => { 25 | set( 26 | produce((state: IStageStore): void => { 27 | if (!isUndefined(stage.prompt)) { 28 | if (isNull(stage.prompt)) { 29 | state.prompts[chatId] = null; 30 | } else { 31 | state.prompts[chatId] = stage.prompt; 32 | } 33 | } 34 | if (!isUndefined(stage.input)) { 35 | state.inputs[chatId] = stage.input || ''; 36 | } 37 | }) 38 | ); 39 | }, 40 | deleteStage: (chatId: string) => { 41 | set( 42 | produce((state: IStageStore): void => { 43 | delete state.prompts[chatId]; 44 | delete state.inputs[chatId]; 45 | }) 46 | ); 47 | }, 48 | })); 49 | 50 | export default useStageStore; 51 | -------------------------------------------------------------------------------- /src/intellichat/services/FireChatService.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { 3 | IChatContext, 4 | IChatRequestMessage, 5 | IChatResponseMessage, 6 | } from 'intellichat/types'; 7 | 8 | import OpenAIChatService from './OpenAIChatService'; 9 | import Fire from 'providers/Fire'; 10 | import useAuthStore from 'stores/useAuthStore'; 11 | import INextChatService from './INextCharService'; 12 | import FireReader from 'intellichat/readers/FireReader'; 13 | 14 | const debug = Debug('5ire:intellichat:FireChatService'); 15 | 16 | export default class FireChatService 17 | extends OpenAIChatService 18 | implements INextChatService 19 | { 20 | constructor(context: IChatContext) { 21 | super(context); 22 | this.provider = Fire; 23 | } 24 | 25 | protected getReaderType() { 26 | return FireReader; 27 | } 28 | 29 | private getUserId() { 30 | const { session } = useAuthStore.getState(); 31 | return session?.user.id; 32 | } 33 | 34 | protected async makeRequest( 35 | messages: IChatRequestMessage[] 36 | ): Promise { 37 | const payload = await this.makePayload(messages); 38 | debug('About to make a request, payload:\r\n', payload); 39 | const { base } = this.apiSettings; 40 | const key = this.getUserId(); 41 | if (!key) { 42 | throw new Error('User is not authenticated'); 43 | } 44 | const response = await fetch(`${base}/v1/chat/completions`, { 45 | method: 'POST', 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | Authorization: `Bearer ${key}`, 49 | }, 50 | body: JSON.stringify(payload), 51 | signal: this.abortController.signal, 52 | }); 53 | return response; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/intellichat/validators.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals'; 2 | import { isValidMaxTokens, isValidTemperature } from '../../src/intellichat/validators'; 3 | 4 | describe('intellichat/validators', () => { 5 | test('isValidMaxToken', () => { 6 | const maxToken1 = 4096; 7 | expect(isValidMaxTokens(maxToken1, 'OpenAI', 'gpt-4-1106-preview')).toBe( 8 | true 9 | ); 10 | expect(isValidMaxTokens(maxToken1, 'OpenAI', 'gpt-4-vision-preview')).toBe( 11 | true 12 | ); 13 | expect(isValidMaxTokens(maxToken1, 'OpenAI', 'gpt-4-0613')).toBe(true); 14 | expect(isValidMaxTokens(maxToken1, 'Baidu', 'ERNIE-Bot 4.0')).toBe(true); 15 | 16 | const maxToken2 = 32768; 17 | expect(isValidMaxTokens(maxToken1, 'OpenAI', 'gpt-4-1106-preview')).toBe( 18 | true 19 | ); 20 | expect(isValidMaxTokens(maxToken2, 'OpenAI', 'gpt-4-32k-0613')).toBe(false); 21 | expect(isValidMaxTokens(maxToken2, 'OpenAI', 'gpt-4-0613')).toBe(false); 22 | expect(isValidMaxTokens(maxToken2, 'Baidu', 'ERNIE-Bot')).toBe(false); 23 | 24 | expect(isValidMaxTokens(maxToken2, 'OpenAI', 'ERNIE-Bot 4.0')).toBe(false); 25 | }); 26 | 27 | test('isValidTemperature', () => { 28 | expect(isValidTemperature(1, 'OpenAI')).toBe(true); 29 | expect(isValidTemperature(1, 'Baidu')).toBe(true); 30 | expect(isValidTemperature(0, 'OpenAI')).toBe(true); 31 | expect(isValidTemperature(0, 'Baidu')).toBe(false); 32 | 33 | expect(isValidTemperature(2, 'OpenAI')).toBe(false); 34 | expect(isValidTemperature(2, 'Baidu')).toBe(false); 35 | 36 | expect(isValidTemperature(-1, 'OpenAI')).toBe(false); 37 | expect(isValidTemperature(-1, 'Baidu')).toBe(false); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/renderer/components/layout/aside/AppSidebar.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Sidebar 3 | */ 4 | import { useLocation } from 'react-router-dom'; 5 | import useAppearanceStore from 'stores/useAppearanceStore'; 6 | import GlobalNav from './GlobalNav'; 7 | import ChatNav from './ChatNav'; 8 | import AppNav from './AppNav'; 9 | import Footer from './Footer'; 10 | 11 | import './AppSidebar.scss'; 12 | import BookmarkNav from './BookmarkNav'; 13 | 14 | 15 | export default function Sidebar() { 16 | const location = useLocation(); 17 | const sidebar = useAppearanceStore((state) => state.sidebar); 18 | const width = sidebar.hidden ? 'w-0' : 'w-auto'; 19 | const left = sidebar.hidden ? 'md:left-0' : '-left-64 md:left-0'; 20 | const leftCollapsed = sidebar.hidden ? '-left-64' : '-left-64 md:left-0'; 21 | 22 | 23 | const renderNav = () => { 24 | const activeRoute = location.pathname.split('/')[1]; 25 | switch (activeRoute) { 26 | case 'apps': 27 | return ; 28 | case 'bookmarks': 29 | return ; 30 | default: 31 | return ; 32 | } 33 | }; 34 | 35 | renderNav(); 36 | 37 | return ( 38 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { ProviderType, IChatModel, 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 ChatBro from './ChatBro'; 8 | import Anthropic from './Anthropic'; 9 | import Fire from './Fire'; 10 | import Ollama from './Ollama'; 11 | import { merge } from 'lodash'; 12 | import Doubao from './Doubao'; 13 | import Grok from './Grok'; 14 | import DeepSeek from './DeepSeek'; 15 | 16 | export const providers: { [key: string]: IServiceProvider } = { 17 | OpenAI, 18 | Anthropic, 19 | Azure, 20 | Google, 21 | Grok, 22 | Baidu, 23 | Moonshot, 24 | ChatBro, 25 | Ollama, 26 | Doubao, 27 | DeepSeek, 28 | '5ire':Fire, 29 | }; 30 | 31 | export function getProvider(providerName: ProviderType): IServiceProvider { 32 | return providers[providerName]; 33 | } 34 | 35 | export function getChatModel( 36 | providerName: ProviderType, 37 | modelName: string 38 | ): IChatModel { 39 | const provider = getProvider(providerName); 40 | if(Object.keys(provider.chat.models).length===0){ 41 | return {} as IChatModel; 42 | } 43 | let model = provider.chat.models[modelName]; 44 | return model||{} as IChatModel; 45 | } 46 | 47 | export function getGroupedChatModelNames(): { [key: string]: string[] } { 48 | const group = (models: IChatModel[]) => 49 | models.reduce((acc: { [key: string]: string[] }, cur: IChatModel) => { 50 | if (acc[cur.group]) { 51 | acc[cur.group].push(cur.name); 52 | } else { 53 | acc[cur.group] = [cur.name]; 54 | } 55 | return acc; 56 | }, {}); 57 | const models = Object.values(providers).map((provider: IServiceProvider) => 58 | group(Object.values(provider.chat.models)) 59 | ); 60 | const result={} 61 | merge(result,...models) 62 | return result 63 | } 64 | -------------------------------------------------------------------------------- /test/utils/mcp.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals'; 2 | import * as mcp from '../../src/utils/mcp'; 3 | 4 | describe('mcp', () => { 5 | test('getParameters', () => { 6 | const args1 = ['--db-path', '']; 7 | const params1 = mcp.getParameters(args1); 8 | expect(params1.length).toEqual(1); 9 | expect(params1[0].name).toEqual('dbPath'); 10 | expect(params1[0].type).toEqual('string'); 11 | expect(params1[0].description).toEqual('Sqlite database path'); 12 | 13 | const args2 = ['--db-path', '', '--db-name', '']; 14 | const params2 = mcp.getParameters(args2); 15 | expect(params2.length).toEqual(2); 16 | expect(params2[0].name).toEqual('dbPath'); 17 | expect(params2[0].type).toEqual('string'); 18 | expect(params2[0].description).toEqual('database path'); 19 | expect(params2[1].name).toEqual('dbName'); 20 | expect(params2[1].type).toEqual('string'); 21 | expect(params2[1].description).toEqual('database name') 22 | 23 | const args3 = ['']; 24 | const params3 = mcp.getParameters(args3); 25 | expect(params3).toEqual([]); 26 | }); 27 | 28 | test('setParameters', () => { 29 | const params = { dbPath: 'path/to/db', dbName: '5ire' }; 30 | const args1 = ['--db-path', '']; 31 | const newArgs1 = mcp.setParameters(args1, params); 32 | expect(newArgs1).toEqual(['--db-path', 'path/to/db']); 33 | 34 | const args2 = ['--db-path', '', '--db-name', '']; 35 | const newArgs2 = mcp.setParameters(args2, params); 36 | expect(newArgs2).toEqual(['--db-path', 'path/to/db', '--db-name', '5ire']); 37 | 38 | const args3 = ['']; 39 | const newArgs3 = mcp.setParameters(args3, params); 40 | expect(newArgs3).toEqual(['']); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/renderer/pages/settings/AppearanceSettings.tsx: -------------------------------------------------------------------------------- 1 | import { captureException } from '../../logging'; 2 | import { FormEvent } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { 5 | RadioGroup, 6 | Radio, 7 | RadioGroupOnChangeData, 8 | } from '@fluentui/react-components'; 9 | import { ThemeType } from '../../../types/appearance.d'; 10 | import useSettingsStore from '../../../stores/useSettingsStore'; 11 | import useAppearanceStore from '../../../stores/useAppearanceStore'; 12 | 13 | export default function AppearanceSettings() { 14 | const { t } = useTranslation(); 15 | const { setTheme } = useAppearanceStore(); 16 | const themeSetting = useSettingsStore((state) => state.theme); 17 | const setThemeSetting = useSettingsStore((state) => state.setTheme); 18 | 19 | const onThemeChange = ( 20 | ev: FormEvent, 21 | data: RadioGroupOnChangeData 22 | ) => { 23 | setThemeSetting(data.value as ThemeType); 24 | if (data.value === 'system') { 25 | window.electron 26 | .getNativeTheme() 27 | .then((_theme) => { 28 | return setTheme(_theme as ThemeType); 29 | }) 30 | .catch(captureException); 31 | } else { 32 | setTheme(data.value as ThemeType); 33 | } 34 | }; 35 | return ( 36 |
37 |
{t('Common.Appearance')}
38 |
39 | 44 | 45 | 46 | 47 | 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/mcp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ['--db-path',] => dbPath 3 | */ 4 | 5 | export interface IMCPServerParameter { 6 | name: string; 7 | type: string; 8 | description: string; 9 | } 10 | 11 | function replaceParamInBrackets( 12 | params: { [key: string]: any }, 13 | template: string 14 | ) { 15 | // 使用正则表达式匹配 的模式 16 | return template.replace(/<([^:>]+):[^>]*>/g, (match, key) => { 17 | // 检查 params 中是否存在对应的 key 18 | if (params.hasOwnProperty(key)) { 19 | // 如果存在,则返回 params 中对应 key 的值 20 | return params[key]; 21 | } 22 | // 如果不存在,则返回原始匹配项 23 | return match; 24 | }); 25 | } 26 | 27 | export function getParameters(args: string[]): IMCPServerParameter[] { 28 | const paramRegex = /<([^>]+)>/g; 29 | const params: IMCPServerParameter[] = []; 30 | let match; 31 | while ((match = paramRegex.exec(args.join(' '))) !== null) { 32 | const [name, type, description] = match[1].split(':'); 33 | params.push({ 34 | name, 35 | type: type || 'string', 36 | description: description || '', 37 | }); 38 | } 39 | return params; 40 | } 41 | 42 | export function setParameters( 43 | args: string[], 44 | params: { [key: string]: string } 45 | ): string[] { 46 | let _args = [...args]; 47 | for (const key in params) { 48 | _args = _args.map((arg) => 49 | replaceParamInBrackets({ [key]: params[key] }, arg) 50 | ); 51 | } 52 | return _args; 53 | } 54 | 55 | export function setEnv( 56 | env: Record | undefined, 57 | params: { [key: string]: string } 58 | ): Record { 59 | if (!env) { 60 | return {}; 61 | } 62 | const _env = { ...env }; 63 | for (const key in params) { 64 | const regex = new RegExp('<' + key + '>', 'g'); 65 | for (const envKey in _env) { 66 | _env[envKey] = replaceParamInBrackets( 67 | { [key]: params[key] }, 68 | _env[envKey] 69 | ); 70 | } 71 | } 72 | return _env; 73 | } 74 | -------------------------------------------------------------------------------- /src/intellichat/services/index.ts: -------------------------------------------------------------------------------- 1 | import { IChatContext } from '../types'; 2 | import { ProviderType } from '../../providers/types'; 3 | import AnthropicChatService from './AnthropicChatService'; 4 | import AzureChatService from './AzureChatService'; 5 | import OllamaChatService from './OllamaChatService'; 6 | import OpenAIChatService from './OpenAIChatService'; 7 | import GoogleChatService from './GoogleChatService'; 8 | import BaiduChatService from './BaiduChatService'; 9 | import ChatBroChatService from './ChatBroChatService'; 10 | import MoonshotChatService from './MoonshotChatService'; 11 | import FireChatService from './FireChatService'; 12 | import DoubaoChatService from './DoubaoChatService'; 13 | import GrokChatService from './GrokChatService'; 14 | import DeepSeekChatService from './DeepSeekChatService'; 15 | import INextChatService from './INextCharService'; 16 | 17 | export default function createService( 18 | providerName: ProviderType, 19 | chatCtx: IChatContext 20 | ): INextChatService { 21 | switch (providerName) { 22 | case 'Anthropic': 23 | return new AnthropicChatService(chatCtx); 24 | case 'OpenAI': 25 | return new OpenAIChatService(chatCtx); 26 | case 'Azure': 27 | return new AzureChatService(chatCtx); 28 | case 'Google': 29 | return new GoogleChatService(chatCtx); 30 | case 'Baidu': 31 | return new BaiduChatService(chatCtx); 32 | case 'Moonshot': 33 | return new MoonshotChatService(chatCtx); 34 | case 'Ollama': 35 | return new OllamaChatService(chatCtx); 36 | case 'ChatBro': 37 | return new ChatBroChatService(chatCtx); 38 | case '5ire': 39 | return new FireChatService(chatCtx); 40 | case 'Doubao': 41 | return new DoubaoChatService(chatCtx); 42 | case 'Grok': 43 | return new GrokChatService(chatCtx); 44 | case 'DeepSeek': 45 | return new DeepSeekChatService(chatCtx); 46 | default: 47 | throw new Error(`Invalid provider:${providerName}`); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/renderer/pages/apps/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | import { Image } from '@fluentui/react-components'; 3 | import Empty from '../../components/Empty'; 4 | import apps from '../../apps' 5 | import { IAppConfig } from '../../apps/types' 6 | import useNav from 'hooks/useNav'; 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 |
{app.name}
34 |
{app.description}
35 |
36 |
37 | ))} 38 |
39 | ) : ( 40 | 41 | )} 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /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 | setTheme: (theme: Omit) => void; 13 | toggleSidebarCollapsed: () => void; 14 | toggleSidebarVisibility: () => void; 15 | getPalette: (name: 'success'|'warning'|'error'|'info') => string; 16 | } 17 | 18 | const useAppearanceStore = create((set, get) => ({ 19 | theme: defaultTheme, 20 | sidebar: { 21 | hidden: false, 22 | collapsed: false, 23 | }, 24 | setTheme: (theme: Omit) => set({ theme }), 25 | toggleSidebarCollapsed: () => { 26 | set((state) => { 27 | const collapsed = !state.sidebar.collapsed; 28 | const hidden = false; 29 | localStorage.setItem('collapsed', String(collapsed)); 30 | window.electron.ingestEvent([{ app: 'toggle-sidebar-collapsed' }]); 31 | return { sidebar: { collapsed, hidden } }; 32 | }); 33 | }, 34 | toggleSidebarVisibility: () => { 35 | set((state) => { 36 | const hidden = !state.sidebar.hidden; 37 | const collapsed = false; 38 | localStorage.setItem('hidden', String(hidden)); 39 | window.electron.ingestEvent([{ app: 'toggle-sidebar-visibility' }]); 40 | return { sidebar: { collapsed, hidden } }; 41 | }); 42 | }, 43 | getPalette: (name: 'error'|'warning'|'success'|'info') => { 44 | const light = { 45 | success: '#3d7d3f', 46 | warning: '#d98926', 47 | error: '#c6474e', 48 | info: '#6e747d', 49 | }; 50 | const dark = { 51 | success: '#64b75d', 52 | warning: '#e6a52a', 53 | error: '#de5d43', 54 | info: '#e7edf2', 55 | } 56 | const {theme} = get(); 57 | return theme === 'dark'? dark[name] : light[name]; 58 | } 59 | })); 60 | 61 | export default useAppearanceStore; 62 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 Grid from './Grid'; 7 | import { ICollection } from 'types/knowledge'; 8 | import useKnowledgeStore from 'stores/useKnowledgeStore'; 9 | import { debounce } from 'lodash'; 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 | listCollections().then((collections: ICollection[]) => { 20 | setCollections(collections); 21 | }); 22 | }, 1000, { leading: true }) 23 | ).current; 24 | 25 | useEffect(() => { 26 | debouncedLoad(); 27 | }, [collectionChangedAt]); 28 | 29 | return ( 30 |
31 |
32 |
33 |
34 |

{t('Common.Knowledge')}

35 |
36 | 42 |
43 |
44 |
45 |
46 | {collections.length ? ( 47 |
48 | 49 |
50 | ) : ( 51 | 52 | )} 53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/intellichat/readers/FireReader.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { IChatResponseMessage } from 'intellichat/types'; 3 | import BaseReader from './BaseReader'; 4 | import IChatReader, { ITool, IReadResult } from './IChatReader'; 5 | 6 | const debug = Debug('5ire:intellichat:FireReader'); 7 | 8 | export default class FireReader extends BaseReader implements IChatReader { 9 | protected parseReply(chunk: string): IChatResponseMessage { 10 | return { 11 | content: chunk, 12 | isEnd: false, 13 | }; 14 | } 15 | 16 | protected parseTools(respMsg: IChatResponseMessage): ITool | null { 17 | console.warn('parseTools not implemented'); 18 | return null; 19 | } 20 | 21 | protected parseToolArgs(respMsg: IChatResponseMessage): { 22 | index: number; 23 | args: string; 24 | } | null { 25 | console.warn('parseToolArgs not implemented'); 26 | return null; 27 | } 28 | 29 | public async read({ 30 | onError, 31 | onProgress, 32 | onToolCalls, 33 | }: { 34 | onError: (error: any) => void; 35 | onProgress: (chunk: string) => void; 36 | onToolCalls: (toolCalls: any) => void; 37 | }): Promise { 38 | const decoder = new TextDecoder('utf-8'); 39 | let content = ''; 40 | let done = false; 41 | try { 42 | while (!done) { 43 | /* eslint-disable no-await-in-loop */ 44 | const data = await this.reader.read(); 45 | done = data.done || false; 46 | const value = decoder.decode(data.value); 47 | const chunks = value 48 | .split('data:') 49 | .map((i) => i.replace('\r\n', '')) 50 | .filter((i) => i !== ''); 51 | for (let curChunk of chunks) { 52 | if (curChunk === '[DONE]') { 53 | done = true; 54 | break; 55 | } 56 | const message = this.parseReply(curChunk); 57 | content += message.content; 58 | onProgress(message.content || ''); 59 | } 60 | } 61 | } catch (err) { 62 | console.error('Read error:', err); 63 | onError(err); 64 | } finally { 65 | return { 66 | content, 67 | }; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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 { useTranslation } from 'react-i18next'; 12 | import { Dismiss24Regular } from '@fluentui/react-icons'; 13 | import { useCallback } 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 { t } = useTranslation(); 24 | const confirm = useCallback(() => { 25 | async function delAndClose() { 26 | await onConfirm(); 27 | setOpen(false); 28 | } 29 | delAndClose(); 30 | }, [setOpen, onConfirm]); 31 | 32 | return ( 33 | 34 | 35 | 36 | 39 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/providers/Fire.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | export default { 4 | name: '5ire', 5 | apiBase: 'https://skyfire.agisurge.com', 6 | //apiBase: 'http://127.0.0.1:8000', 7 | currency: 'USD', 8 | isPremium: true, 9 | options: { 10 | apiBaseCustomizable: false, 11 | apiKeyCustomizable: false, 12 | }, 13 | chat: { 14 | apiSchema: ['base', 'model'], 15 | presencePenalty: { min: -2, max: 2, default: 0 }, 16 | topP: { min: 0, max: 1, default: 1 }, 17 | temperature: { min: 0, max: 1, default: 0.9 }, 18 | options: { 19 | modelCustomizable: true, 20 | }, 21 | models: { 22 | /** 由于暂不支持 streaming,所以暂时不支持 o1 系列 23 | 'o1-preview': { 24 | name: 'o1-preview', 25 | contextWindow: 128000, 26 | maxTokens: 32768, 27 | inputPrice: 0.015, 28 | outputPrice: 0.06, 29 | vision: { 30 | enabled: false, 31 | }, 32 | jsonModelEnabled: false, 33 | description: ``, 34 | group: 'o1', 35 | }, 36 | 'o1-mini': { 37 | name: 'o1-mini', 38 | contextWindow: 128000, 39 | maxTokens: 65536, 40 | inputPrice: 0.003, 41 | outputPrice: 0.012, 42 | vision: { 43 | enabled: false, 44 | }, 45 | jsonModelEnabled: false, 46 | description: ``, 47 | group: 'o1', 48 | }, 49 | */ 50 | 'gpt-4o': { 51 | name: 'gpt-4o', 52 | contextWindow: 128000, 53 | maxTokens: 4096, 54 | inputPrice: 0.005, 55 | outputPrice: 0.015, 56 | vision: { 57 | enabled: true, 58 | }, 59 | jsonModelEnabled: false, 60 | description: ``, 61 | group: 'GPT-4', 62 | }, 63 | 'gpt-4': { 64 | name: 'gpt-4', 65 | contextWindow: 128000, 66 | maxTokens: 4096, 67 | inputPrice: 0.03, 68 | outputPrice: 0.06, 69 | group: 'GPT-4', 70 | }, 71 | 'gpt-3.5-turbo': { 72 | name: 'gpt-35-turbo', 73 | contextWindow: 16385, 74 | maxTokens: 4096, 75 | inputPrice: 0.0015, 76 | outputPrice: 0.002, 77 | group: 'GPT-3.5', 78 | }, 79 | }, 80 | }, 81 | } as IServiceProvider; 82 | -------------------------------------------------------------------------------- /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-bg-sidebar: 238, 236, 235; 8 | --color-accent: 63, 118, 255; 9 | --color-text-base: 3, 7, 18; 10 | --color-text-secondary: 55, 65, 81; 11 | --color-text-success: 61, 125, 63; 12 | --color-text-warning: 217, 137, 38; 13 | --color-text-danger: 198, 71, 78; 14 | --color-text-info: 102, 109, 117; 15 | color-scheme: light; 16 | } 17 | 18 | [data-theme='light-contrast'] { 19 | --color-bg-base: 250, 250, 250; 20 | --color-bg-surface-1: 250, 250, 250; 21 | --color-bg-surface-2: 206, 212, 218; 22 | --color-border: 0, 0, 0; 23 | --color-bg-sidebar: 255, 255, 255; 24 | --color-accent: 63, 118, 255; 25 | --color-text-base: 0, 0, 0; 26 | --color-text-secondary: 0, 0, 0; 27 | --color-text-success: 61, 125, 63; 28 | --color-text-warning: 217, 137, 38; 29 | --color-text-danger: 198, 71, 78; 30 | --color-text-info: 102, 109, 117; 31 | color-scheme: light; 32 | } 33 | 34 | [data-theme='dark'] { 35 | --color-bg-base: 25, 27, 27; 36 | --color-bg-surface-1: 46, 46, 46; 37 | --color-bg-surface-2: 48, 49, 52; 38 | --color-bg-surface-3: 49, 54, 58; 39 | --color-border: 36, 36, 36; 40 | --color-bg-sidebar: 44, 42, 43; 41 | --color-accent: 60, 133, 217; 42 | --color-text-base: 200, 214, 222; 43 | --color-text-secondary: 142, 148, 146; 44 | --color-text-success: 67, 132, 64; 45 | --color-text-warning: 230, 165, 42; 46 | --color-text-danger: 231, 121, 117; 47 | --color-text-info: 126, 133, 143; 48 | color-scheme: dark; 49 | } 50 | 51 | [data-theme='dark-contrast'] { 52 | --color-bg-base: 25, 27, 27; 53 | --color-bg-surface-1: 31, 32, 35; 54 | --color-bg-surface-2: 39, 42, 45; 55 | --color-border: 255, 255, 255; 56 | --color-bg-sidebar: 19, 20, 22; 57 | --color-accent: 60, 133, 217; 58 | --color-text-base: 255, 255, 255; 59 | --color-text-secondary: 142, 148, 146; 60 | --color-text-success: 67, 132, 64; 61 | --color-text-warning: 230, 165, 42; 62 | --color-text-danger: 231, 121, 117; 63 | --color-text-info: 126, 133, 143; 64 | color-scheme: dark; 65 | } 66 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /src/hooks/useMarkdown.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-danger */ 2 | 3 | // @ts-ignore 4 | import MarkdownIt from 'markdown-it'; 5 | // @ts-ignore 6 | import mathjax3 from 'markdown-it-mathjax3'; // @ts-ignore 7 | import hljs from 'highlight.js/lib/common'; 8 | 9 | export default function useMarkdown() { 10 | const md = new MarkdownIt({ 11 | html: true, 12 | linkify: true, 13 | typographer: true, 14 | highlight(str: string, lang: string) { 15 | // notice: 硬编码解决 ellipsis-loader 被转移为代码显示的问题。 16 | const loader = ''; 17 | const isLoading = str.indexOf(loader) > -1; 18 | let code = str; 19 | if (isLoading) { 20 | code = str.replace(loader, ''); 21 | } 22 | if (lang && hljs.getLanguage(lang)) { 23 | try { 24 | return ( 25 | `
` +
26 |             `${hljs.highlight(lang, code, true).value}${
27 |               isLoading ? loader : ''
28 |             }
` 29 | ); 30 | } catch (__) { 31 | return ( 32 | `
` +
33 |             `${hljs.highlightAuto(code).value}${
34 |               isLoading ? loader : ''
35 |             }` +
36 |             `
` 37 | ); 38 | } 39 | } 40 | return ( 41 | `
` +
42 |         `${hljs.highlightAuto(code).value}${
43 |           isLoading ? loader : ''
44 |         }` +
45 |         `
` 46 | ); 47 | }, 48 | }).use(mathjax3); 49 | const defaultRender = 50 | md.renderer.rules.link_open || 51 | function (tokens: any, idx: any, options: any, env: any, self: any) { 52 | return self.renderToken(tokens, idx, options); 53 | }; 54 | md.renderer.rules.link_open = function ( 55 | tokens: any, 56 | idx: any, 57 | options: any, 58 | env: any, 59 | self: any 60 | ) { 61 | // Add a new `target` attribute, or replace the value of the existing one. 62 | tokens[idx].attrSet('target', '_blank'); 63 | // Pass the token to the default renderer. 64 | return defaultRender(tokens, idx, options, env, self); 65 | }; 66 | return { 67 | render: (str: string): string => md.render(str), 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/intellichat/services/ChatBroChatService.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { 3 | IChatContext, 4 | IChatRequestMessage, 5 | IChatRequestPayload, 6 | } from 'intellichat/types'; 7 | import ChatBro from '../../providers/ChatBro'; 8 | import INextChatService from './INextCharService'; 9 | import OpenAIChatService from './OpenAIChatService'; 10 | import ChatBroReader from 'intellichat/readers/ChatBroReader'; 11 | 12 | const debug = Debug('5ire:intellichat:ChatBroChatService'); 13 | 14 | export default class ChatBroChatService 15 | extends OpenAIChatService 16 | implements INextChatService 17 | { 18 | constructor(context: IChatContext) { 19 | super(context); 20 | this.provider = ChatBro; 21 | } 22 | 23 | protected getReaderType() { 24 | return ChatBroReader; 25 | } 26 | 27 | protected async makePayload( 28 | messages: IChatRequestMessage[] 29 | ): Promise { 30 | const payload: IChatRequestPayload = { 31 | model: this.context.getModel().name, 32 | messages: await this.makeMessages(messages), 33 | temperature: this.context.getTemperature(), 34 | stream: true, 35 | }; 36 | if (this.context.getMaxTokens()) { 37 | payload.max_tokens = this.context.getMaxTokens(); 38 | } 39 | debug('payload', payload); 40 | return payload; 41 | } 42 | 43 | protected async makeRequest( 44 | messages: IChatRequestMessage[] 45 | ): Promise { 46 | const payload = await this.makePayload(messages); 47 | debug('About to make a request, payload:\r\n', payload); 48 | const { base, key } = this.apiSettings; 49 | const postResp = await fetch(`${base}/v1/open/azure/chat`, { 50 | method: 'POST', 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | 'x-api-key': key, 54 | }, 55 | body: JSON.stringify(payload), 56 | }); 57 | const data: any = await postResp.json(); 58 | const response = await fetch( 59 | `${base}/v1/open/azure/stream/chat/${data.key}`, 60 | { 61 | method: 'GET', 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | 'x-api-key': key, 65 | }, 66 | signal: this.abortController.signal, 67 | } 68 | ); 69 | return response; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/components/layout/aside/ChatNav.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import useNav from 'hooks/useNav'; 3 | import { Button, Tooltip } from '@fluentui/react-components'; 4 | import { Chat20Regular, Chat20Filled } from '@fluentui/react-icons'; 5 | import useChatStore from 'stores/useChatStore'; 6 | import { IChat } from 'intellichat/types'; 7 | 8 | export default function ChatNav({ collapsed }: { collapsed: boolean }) { 9 | const chats = useChatStore((state) => state.chats); 10 | const currentChat = useChatStore((state) => state.chat); 11 | const fetchChat = useChatStore((state: any) => state.fetchChat); 12 | 13 | useEffect(() => { 14 | fetchChat(); 15 | }, [fetchChat]); 16 | 17 | const navigate = useNav(); 18 | 19 | const renderIconWithTooltip = (isActiveChat: boolean, summary: string) => { 20 | return ( 21 | 27 | {isActiveChat ? : } 28 | 29 | ); 30 | }; 31 | 32 | return ( 33 |
34 |
37 | {chats.map((chat: IChat) => { 38 | return ( 39 |
45 | 60 |
61 | ); 62 | })} 63 |
64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/hooks/useProvider.ts: -------------------------------------------------------------------------------- 1 | import { providers } from '../providers'; 2 | import { IChatModel, IServiceProvider, ProviderType } from 'providers/types'; 3 | import useAuthStore from 'stores/useAuthStore'; 4 | 5 | export default function useProvider() { 6 | 7 | const {session} = useAuthStore.getState() 8 | 9 | function getProviders(arg?:{withDisabled:boolean}): { [key: string]: IServiceProvider } { 10 | return Object.values(providers).reduce( 11 | (acc: { [key: string]: IServiceProvider }, cur: IServiceProvider) => { 12 | if(!arg?.withDisabled && cur.disabled) return acc; 13 | if (!!session || !cur.isPremium) { 14 | acc[cur.name] = cur; 15 | } 16 | return acc; 17 | }, 18 | {} as { [key: string]: IServiceProvider } 19 | ); 20 | } 21 | 22 | function getProvider(providerName: ProviderType): IServiceProvider { 23 | const providers = getProviders(); 24 | let provider = providers[providerName]; 25 | if (!provider) { 26 | return Object.values(providers)[0]; 27 | } 28 | return provider; 29 | } 30 | 31 | function getDefaultChatModel(provider: ProviderType): IChatModel { 32 | const models = getChatModels(provider) 33 | if(models.length === 0) return {} as IChatModel; 34 | const defaultModel = models.filter((m: IChatModel) => m.isDefault)[0]; 35 | return defaultModel || models[0]; 36 | } 37 | 38 | function getChatModels(providerName: ProviderType): IChatModel[] { 39 | const provider = getProvider(providerName); 40 | return Object.keys(provider.chat.models).map((modelKey) => { 41 | const model = provider.chat.models[modelKey]; 42 | model.label = modelKey; 43 | return model; 44 | }); 45 | } 46 | 47 | function getChatModel( 48 | providerName: ProviderType, 49 | modelLabel: string 50 | ): IChatModel { 51 | const _providers = getProviders(); 52 | let provider = _providers[providerName]; 53 | if (!provider) { 54 | provider = Object.values(_providers)[0]; 55 | } 56 | let model = provider.chat.models[modelLabel]; 57 | if (!model) { 58 | model = getDefaultChatModel(providerName); 59 | }else{ 60 | model.label = modelLabel; 61 | } 62 | return model; 63 | } 64 | 65 | return { 66 | getProviders, 67 | getProvider, 68 | getChatModels, 69 | getChatModel, 70 | getDefaultChatModel, 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /src/renderer/pages/settings/Version.tsx: -------------------------------------------------------------------------------- 1 | import { captureException } from '../../logging'; 2 | import { useEffect, useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import Spinner from 'renderer/components/Spinner'; 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: NodeJS.Timer | null = null; 21 | let updateInfo = window.electron.store.get('updateInfo'); 22 | setUpdateInfo(updateInfo); 23 | if (updateInfo?.isDownloading) { 24 | timer = setInterval(() => { 25 | updateInfo = window.electron.store.get('updateInfo'); 26 | if (timer && !updateInfo?.isDownloading) { 27 | clearInterval(timer); 28 | } 29 | setUpdateInfo(updateInfo); 30 | }, 1000); 31 | 32 | } 33 | window.electron 34 | .getAppVersion() 35 | .then((appVersion) => { 36 | return setVersion(appVersion); 37 | }) 38 | .catch(captureException); 39 | 40 | return () => { 41 | if (timer) { 42 | clearInterval(timer); 43 | } 44 | }; 45 | }, []); 46 | 47 | return ( 48 |
49 |
{t('Common.Version')}
50 |
51 |
{version}
52 | {updateInfo && ( 53 |
54 | {updateInfo?.isDownloading ? ( 55 | <> 56 |
{t('Version.HasNewVersion')}
57 |
58 | 59 | {t('Common.Downloading')} 60 |
61 | 62 | ) : ( 63 |
64 | {updateInfo?.version} will be installed after you restart the 65 | app. 66 |
67 | )} 68 |
69 | )} 70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /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/main/docloader.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import * as logging from './logging'; 3 | import pdf from 'pdf-parse'; 4 | import officeParser from 'officeparser'; 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/stores/useSettingsStore.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import Debug from 'debug'; 3 | import { create } from 'zustand'; 4 | import { isNil, isNull, pick } from 'lodash'; 5 | 6 | import { ThemeType } from '../types/appearance'; 7 | import { IAPISettings, ISettings } from '../types/settings'; 8 | import { getProvider } from 'providers'; 9 | 10 | const debug = Debug('5ire:stores:useSettingsStore'); 11 | 12 | const defaultTheme = 'system'; 13 | 14 | const defaultAPI: IAPISettings = { 15 | provider: 'OpenAI', 16 | base: 'https://api.openai.com', 17 | key: '', 18 | model: '', 19 | }; 20 | 21 | export interface ISettingStore { 22 | theme: ThemeType; 23 | api: IAPISettings; 24 | setTheme: (theme: ThemeType) => void; 25 | setAPI: (api: Partial) => void; 26 | } 27 | 28 | const settings = window.electron.store.get('settings', {}) as ISettings; 29 | let apiSettings = defaultAPI; 30 | if (settings.api?.activeProvider) { 31 | apiSettings = 32 | settings.api.providers[settings.api.activeProvider] || defaultAPI; 33 | } 34 | 35 | const useSettingsStore = create((set, get) => ({ 36 | theme: settings?.theme || defaultTheme, 37 | api: apiSettings, 38 | setTheme: async (theme: ThemeType) => { 39 | set({ theme }); 40 | window.electron.store.set('settings.theme', theme); 41 | }, 42 | setAPI: (api: Partial) => { 43 | set((state) => { 44 | const provider = isNil(api.provider) ? state.api.provider : api.provider; 45 | const base = isNil(api.base) ? state.api.base : api.base; 46 | const key = isNil(api.key) ? state.api.key : api.key; 47 | const secret = isNil(api.secret) ? state.api.secret : api.secret; 48 | const model = isNil(api.model) ? state.api.model: api.model; 49 | const deploymentId = isNil(api.deploymentId) 50 | ? state.api.deploymentId 51 | : api.deploymentId; 52 | const newAPI = { 53 | provider, 54 | base, 55 | key, 56 | secret, 57 | deploymentId, 58 | model, 59 | } as IAPISettings; 60 | const { apiSchema } = getProvider(provider).chat; 61 | window.electron.store.set('settings.api.activeProvider', provider); 62 | window.electron.store.set( 63 | `settings.api.providers.${provider}`, 64 | pick(newAPI, [...apiSchema, 'provider']) 65 | ); 66 | return { api: newAPI }; 67 | }); 68 | }, 69 | })); 70 | 71 | export default useSettingsStore; 72 | -------------------------------------------------------------------------------- /src/intellichat/readers/ChatBroReader.ts: -------------------------------------------------------------------------------- 1 | import { IChatResponseMessage } from 'intellichat/types'; 2 | 3 | import IChatReader, { IReadResult, ITool } from './IChatReader'; 4 | import OpenAIReader from './OpenAIReader'; 5 | 6 | export default class ChatBroReader extends OpenAIReader implements IChatReader { 7 | protected parseReply(chunk: string): IChatResponseMessage { 8 | if (chunk === '[DONE]') { 9 | return { 10 | content: '', 11 | isEnd: true, 12 | }; 13 | } 14 | return { 15 | content: chunk, 16 | isEnd: false, 17 | }; 18 | } 19 | 20 | protected parseTools(respMsg: IChatResponseMessage): ITool | null { 21 | console.warn('parseTools not implemented'); 22 | return null; 23 | } 24 | 25 | protected parseToolArgs(respMsg: IChatResponseMessage): { 26 | index: number; 27 | args: string; 28 | } | null { 29 | console.warn('parseToolArgs not implemented'); 30 | return null; 31 | } 32 | 33 | public async read({ 34 | onError, 35 | onProgress, 36 | onToolCalls, 37 | }: { 38 | onError: (error: any) => void; 39 | onProgress: (chunk: string) => void; 40 | onToolCalls: (toolCalls: any) => void; 41 | }): Promise { 42 | const decoder = new TextDecoder('utf-8'); 43 | let content = ''; 44 | let done = false; 45 | try { 46 | while (!done) { 47 | /* eslint-disable no-await-in-loop */ 48 | const data = await this.reader.read(); 49 | done = data.done || false; 50 | const value = decoder.decode(data.value); 51 | const lines = value 52 | .split('\n') 53 | .map((i) => i.trim()) 54 | .filter((i) => i !== ''); 55 | for (const line of lines) { 56 | const chunks = line 57 | .split('data:') 58 | .filter((i) => i !== '') 59 | .map((i) => i.trim()); 60 | for (let curChunk of chunks) { 61 | let chunk = decodeURIComponent(curChunk); 62 | if (chunk === '[DONE]') { 63 | done = true; 64 | break; 65 | } 66 | const message = this.parseReply(chunk); 67 | content += message.content; 68 | onProgress(message.content || ''); 69 | } 70 | } 71 | } 72 | } catch (err) { 73 | console.error('Read error:', err); 74 | onError(err); 75 | } finally { 76 | return { 77 | content, 78 | }; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/utils/validators.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals'; 2 | import { isValidUsername, isValidEmail, isValidPassword } from '../../src/utils/validators'; 3 | 4 | describe('utils/validators', () => { 5 | test('isValidUsername', () => { 6 | expect(isValidUsername('你好')).toBe(true); 7 | expect(isValidUsername('你')).toBe(false); 8 | expect(isValidUsername('Jack')).toBe(true); 9 | expect(isValidUsername('')).toBe(false); 10 | expect(isValidUsername('Jack-')).toBe(false); 11 | expect(isValidUsername('Jack-1')).toBe(false); 12 | expect(isValidUsername('Jack.Tom')).toBe(true); 13 | expect(isValidUsername('Jack.')).toBe(false); 14 | expect(isValidUsername('.Jack')).toBe(false); 15 | expect(isValidUsername('Jack.Tom.')).toBe(false); 16 | expect(isValidUsername('Ross*Jack')).toBe(false); 17 | expect(isValidUsername('Ross&Jack')).toBe(false); 18 | expect(isValidUsername('123456789098765432101')).toBe(false); 19 | expect(isValidUsername('1123')).toBe(true); 20 | expect(isValidUsername("Ross'd")).toBe(false); 21 | expect(isValidUsername('Ross"d')).toBe(false); 22 | expect(isValidUsername('Ross+Jack')).toBe(false); 23 | expect(isValidUsername('Ross‘Jack')).toBe(false) 24 | expect(isValidUsername('Ross“Jack')).toBe(false) 25 | expect(isValidUsername('Ross?')).toBe(false); 26 | expect(isValidUsername('Ross*T')).toBe(false); 27 | expect(isValidUsername(' ')).toBe(false); 28 | }); 29 | 30 | test('isValidEmail', ()=>{ 31 | expect(isValidEmail('you@company.com')).toBe(true) 32 | expect(isValidEmail('you+23@company.cn')).toBe(true) 33 | expect(isValidEmail('you@company.co')).toBe(true) 34 | expect(isValidEmail('name.lastname@company.com')).toBe(true) 35 | expect(isValidEmail('you@company.com.cn')).toBe(true) 36 | expect(isValidEmail('you@company')).toBe(false) 37 | expect(isValidEmail('you@@company.com')).toBe(false) 38 | expect(isValidEmail('@company')).toBe(false) 39 | expect(isValidEmail('company')).toBe(false) 40 | expect(isValidEmail('you@name@compan.com')).toBe(false) 41 | }) 42 | 43 | test('isValidPassword',()=>{ 44 | expect(isValidPassword('WX1342')).toBe(true); 45 | expect(isValidPassword('WX134')).toBe(false); 46 | expect(isValidPassword('134423')).toBe(false); 47 | expect(isValidPassword('ewjfhsdakjh')).toBe(false); 48 | expect(isValidPassword('Gd2303')).toBe(true); 49 | expect(isValidPassword('Pass12345678909876541')).toBe(false); 50 | }) 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /src/renderer/pages/user/TabPassword.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Field, 3 | InputOnChangeData, 4 | } from '@fluentui/react-components'; 5 | import { Password20Regular } from '@fluentui/react-icons'; 6 | import useToast from 'hooks/useToast'; 7 | import { ChangeEvent, useState } from 'react'; 8 | import { useTranslation } from 'react-i18next'; 9 | import MaskableStateInput from 'renderer/components/MaskableStateInput'; 10 | import StateButton from 'renderer/components/StateButton'; 11 | import { isValidPassword } from 'utils/validators'; 12 | import supabase from 'vendors/supa'; 13 | 14 | export default function TabPassword() { 15 | const { t } = useTranslation(); 16 | const [loading, setLoading] = useState(false); 17 | const [password, setPassword] = useState(''); 18 | const [isPasswordValid, setIsPasswordValid] = useState(true); 19 | 20 | const { notifyError, notifySuccess } = useToast(); 21 | 22 | const updatePassword = async () => { 23 | if(!isValidPassword(password)) return; 24 | setLoading(true); 25 | const { error } = await supabase.auth.updateUser({ password }); 26 | if (error) { 27 | notifyError(error.message); 28 | } else { 29 | notifySuccess(t('Account.Notification.PasswordChanged')); 30 | setIsPasswordValid(true); 31 | setPassword('') 32 | } 33 | setLoading(false); 34 | }; 35 | return ( 36 |
37 |
38 | {t('Common.ChangePassword')} 39 |
40 | 41 | { 44 | setIsPasswordValid(isValidPassword(password)); 45 | }} 46 | isValid={isPasswordValid} 47 | icon={} 48 | errorMsg={t('Account.Info.PasswordRule')} 49 | value={password} 50 | onChange={( 51 | _ev: ChangeEvent, 52 | data: InputOnChangeData 53 | ) => { 54 | setPassword(data.value) 55 | setIsPasswordValid(true); 56 | }} 57 | /> 58 | 59 |
{t('Account.Info.PasswordRule')}
60 |
61 | 66 | {t('Common.Save')} 67 | 68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/stores/useUsageStore.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { ProviderType } from 'providers/types'; 3 | import { typeid } from 'typeid-js'; 4 | import { IUsage, IUsageStatistics } from 'types/usage'; 5 | import { date2unix } from 'utils/util'; 6 | import { create } from 'zustand'; 7 | import { getChatModel } from 'providers'; 8 | 9 | const debug = Debug('5ire:stores:useUsageStore'); 10 | 11 | export interface IUsageStore { 12 | create: (usage: Partial) => Promise; 13 | statistics: ( 14 | startDateUnix: number, 15 | endDateUnix: number 16 | ) => Promise; 17 | } 18 | 19 | const getModelPrice = ( 20 | providerName: ProviderType, 21 | modelName: string, 22 | type: 'input' | 'output' 23 | ) => { 24 | if (type === 'input') { 25 | return getChatModel(providerName, modelName).inputPrice; 26 | } 27 | return getChatModel(providerName, modelName).outputPrice; 28 | }; 29 | 30 | const useUsageStore = create(() => ({ 31 | create: async (usage: Partial) => { 32 | const $usage = { 33 | ...usage, 34 | id: typeid('usg').toString(), 35 | createdAt: date2unix(new Date()), 36 | } as IUsage; 37 | debug('Create a usage ', $usage); 38 | const ok = await window.electron.db.run( 39 | `INSERT INTO usages (id, provider,model, inputTokens, outputTokens, inputPrice, outputPrice,createdAt) 40 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 41 | [ 42 | $usage.id, 43 | $usage.provider, 44 | $usage.model, 45 | $usage.inputTokens, 46 | $usage.outputTokens, 47 | getModelPrice($usage.provider, $usage.model, 'input'), 48 | getModelPrice($usage.provider, $usage.model, 'output'), 49 | $usage.createdAt, 50 | ] 51 | ); 52 | if (!ok) { 53 | throw new Error('Write the usage into database failed'); 54 | } 55 | return $usage; 56 | }, 57 | statistics: async (startDateUnix: number, endDateUnix: number) => { 58 | return (await window.electron.db.all( 59 | ` 60 | SELECT 61 | provider, 62 | model, 63 | sum(inputTokens) inputTokens, 64 | sum(outputTokens) outputTokens, 65 | round(sum(inputTokens * inputPrice / 1000), 4) AS inputCost, 66 | round(sum(outputTokens * outputPrice / 1000), 4) AS outputCost 67 | FROM 68 | usages 69 | WHERE 70 | createdAt >= ? AND createdAt <= ? 71 | GROUP BY 72 | provider, model`, 73 | [startDateUnix, endDateUnix] 74 | )) as IUsageStatistics[]; 75 | }, 76 | })); 77 | 78 | export default useUsageStore; 79 | -------------------------------------------------------------------------------- /src/providers/ChatBro.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | export default { 4 | name: 'ChatBro', 5 | apiBase: 'https://api.chatbro.cn', 6 | currency: 'CNY', 7 | options: { 8 | apiBaseCustomizable: false, 9 | apiKeyCustomizable: true, 10 | }, 11 | chat: { 12 | apiSchema: ['base', 'key', 'model'], 13 | presencePenalty: { min: -2, max: 2, default: 0 }, 14 | topP: { min: 0, max: 1, default: 1 }, 15 | temperature: { min: 0, max: 1, default: 0.9 }, 16 | options: { 17 | modelCustomizable: true, 18 | }, 19 | models: { 20 | 'gpt-4o': { 21 | name: 'gpt-4o', 22 | contextWindow: 128000, 23 | maxTokens: 4906, 24 | inputPrice: 0.005, 25 | outputPrice: 0.015, 26 | description: `GPT-4o it's most advanced multimodal model of OpenAI that’s faster and cheaper than GPT-4 Turbo with stronger vision capabilities`, 27 | group: 'GPT-4', 28 | }, 29 | 'gpt-4o-mini': { 30 | name: 'gpt-4o-mini', 31 | contextWindow: 128000, 32 | maxTokens: 16384, 33 | inputPrice: 0.00015, 34 | outputPrice: 0.0006, 35 | vision: { 36 | enabled: true, 37 | allowBase64: true, 38 | allowUrl: true, 39 | }, 40 | description: `GPT-4o mini (“o” for “omni”) is OpenAI's advanced model in the small models category, and it's cheapest model yet. It is multimodal (accepting text or image inputs and outputting text), has higher intelligence than gpt-3.5-turbo but is just as fast. It is meant to be used for smaller tasks, including vision tasks.`, 41 | group: 'GPT-4', 42 | }, 43 | 'gpt-4': { 44 | name: 'gpt-4', 45 | contextWindow: 128000, 46 | maxTokens: 4906, 47 | inputPrice: 0.08, 48 | outputPrice: 0.3, 49 | description: `The latest GPT-4 model with improved instruction following, 50 | JSON mode, reproducible outputs, parallel function calling, 51 | and more. Returns a maximum of 4,096 output tokens. 52 | This preview model is not yet suited for production traffic`, 53 | group: 'GPT-4', 54 | }, 55 | 'gpt-35-turbo': { 56 | name: 'gpt-35-turbo', 57 | contextWindow: 128000, 58 | maxTokens: 4906, 59 | inputPrice: 0.01, 60 | outputPrice: 0.02, 61 | description: `Ability to understand images, in addition to all other GPT-4 Turbo capabilties. 62 | Returns a maximum of 4,096 output tokens. 63 | This is a preview model version and not suited yet for production traffic`, 64 | group: 'GPT-3.5', 65 | }, 66 | }, 67 | }, 68 | } as IServiceProvider; 69 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import * as logging from './logging'; 2 | import Debug from 'debug'; 3 | import FluentApp from './components/FluentApp'; 4 | import './App.scss'; 5 | import './fluentui.scss'; 6 | import useAuthStore from 'stores/useAuthStore'; 7 | import { useEffect } from 'react'; 8 | import useToast from 'hooks/useToast'; 9 | import { useTranslation } from 'react-i18next'; 10 | import useKnowledgeStore from 'stores/useKnowledgeStore'; 11 | import useMCPStore from 'stores/useMCPStore'; 12 | 13 | if (window.envVars.NODE_ENV === 'development') { 14 | Debug.enable('5ire:*'); 15 | } 16 | 17 | const debug = Debug('5ire:App'); 18 | 19 | logging.init(); 20 | 21 | export default function App() { 22 | const loadAuthData = useAuthStore((state) => state.load); 23 | const setSession = useAuthStore((state) => state.setSession); 24 | const {setActiveServerNames} = useMCPStore(); 25 | const { onAuthStateChange } = useAuthStore(); 26 | const { notifyError } = useToast(); 27 | const { t } = useTranslation(); 28 | const { createFile } = useKnowledgeStore(); 29 | 30 | useEffect(() => { 31 | loadAuthData(); 32 | const subscription = onAuthStateChange(); 33 | 34 | window.electron.ipcRenderer.on('mcp-server-loaded', async (serverNames: any) => { 35 | debug('🚩 MCP Server Loaded:', serverNames); 36 | setActiveServerNames(serverNames); 37 | }); 38 | 39 | window.electron.ipcRenderer.on('sign-in', async (authData: any) => { 40 | if (authData.accessToken && authData.refreshToken) { 41 | const { error } = await setSession(authData); 42 | if (error) { 43 | notifyError(error.message); 44 | } 45 | } else { 46 | debug('🚩 Invalid Auth Data:', authData); 47 | notifyError(t('Auth.Notification.LoginCallbackFailed')); 48 | } 49 | }); 50 | 51 | /** 52 | * 当知识库导入任务完成时触发 53 | * 放這是为了避免组件卸载后无法接收到事件 54 | */ 55 | window.electron.ipcRenderer.on( 56 | 'knowledge-import-success', 57 | (data: unknown) => { 58 | const { collectionId, file, numOfChunks } = data as any; 59 | createFile({ 60 | id: file.id, 61 | collectionId: collectionId, 62 | name: file.name, 63 | size: file.size, 64 | numOfChunks, 65 | }); 66 | } 67 | ); 68 | 69 | return () => { 70 | window.electron.ipcRenderer.unsubscribeAll('mcp-server-loaded'); 71 | window.electron.ipcRenderer.unsubscribeAll('sign-in'); 72 | window.electron.ipcRenderer.unsubscribeAll('knowledge-import-success'); 73 | subscription.unsubscribe() 74 | }; 75 | }, [loadAuthData, onAuthStateChange]); 76 | return ; 77 | } 78 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/renderer/components/layout/aside/BookmarkNav.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Tooltip } from '@fluentui/react-components'; 2 | import { Bookmark20Filled, Bookmark20Regular } from '@fluentui/react-icons'; 3 | import { useEffect } from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | import useNav from 'hooks/useNav'; 6 | import useBookmarkStore from 'stores/useBookmarkStore'; 7 | import { IBookmark } from 'types/bookmark'; 8 | 9 | export default function BookmarkNav({ collapsed }: { collapsed: boolean }) { 10 | const { t } = useTranslation(); 11 | const activeBookmarkId = useBookmarkStore((state) => state.activeBookmarkId); 12 | const favorites = useBookmarkStore((state) => state.favorites); 13 | const loadFavorites = useBookmarkStore((state) => state.loadFavorites); 14 | const navigate = useNav(); 15 | 16 | useEffect(() => { 17 | loadFavorites({ limit: 100, offset: 0 }); 18 | }, [loadFavorites]); 19 | 20 | const renderIconWithTooltip = ( 21 | isActiveBookmark: boolean, 22 | summary: string 23 | ) => { 24 | return ( 25 | 31 | {isActiveBookmark ? : } 32 | 33 | ); 34 | }; 35 | 36 | const renderFavorites = () => { 37 | if (favorites?.length > 0) { 38 | return favorites.map((bookmark: IBookmark) => { 39 | return ( 40 |
48 | 61 |
62 | ); 63 | }); 64 | } 65 | return ( 66 |
67 | {t('Your favorite bookmarkes.')} 68 |
69 | ); 70 | }; 71 | 72 | return ( 73 |
74 |
77 | {renderFavorites()} 78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /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 { 12 | CheckmarkCircle24Filled, 13 | Warning24Filled, 14 | ErrorCircle24Filled, 15 | Info24Filled, 16 | } from '@fluentui/react-icons'; 17 | import { useCallback } from 'react'; 18 | import { useTranslation } from 'react-i18next'; 19 | import useAppearanceStore from 'stores/useAppearanceStore'; 20 | 21 | export default function AlertDialog(args: { 22 | type: 'success' | 'warning' | 'error' | 'info'; 23 | open: boolean; 24 | setOpen: (open: boolean) => void; 25 | title?: string; 26 | message: string; 27 | onConfirm?: () => void; 28 | }) { 29 | const { t } = useTranslation(); 30 | const { type, open, setOpen, title, message, onConfirm } = args; 31 | const getPalette = useAppearanceStore.getState().getPalette; 32 | const renderIcon = useCallback(() => { 33 | switch (type) { 34 | case 'success': 35 | return ( 36 | 40 | ); 41 | case 'warning': 42 | return ( 43 | 47 | ); 48 | case 'error': 49 | return ( 50 | 54 | ); 55 | case 'info': 56 | return ( 57 | 58 | ); 59 | default: 60 | return null; 61 | } 62 | }, [type]); 63 | 64 | return ( 65 | setOpen(data.open)} 69 | > 70 | 71 | 72 | 73 | {renderIcon()} 74 | {title} 75 | 76 | 77 |
78 |
79 | 80 | 81 | 90 | 91 | 92 |
93 |
94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /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({ ctx, chat }: { ctx: IChatContext, chat: IChat}) { 21 | const { t } = useTranslation(); 22 | 23 | const updateChat = useChatStore((state) => state.updateChat); 24 | const editChat = useChatStore((state) => state.editChat); 25 | const [stream, setStream] = useState(true); 26 | 27 | const updateStream = ( 28 | ev: ChangeEvent, 29 | data: SwitchOnChangeData 30 | ) => { 31 | const $stream = data.checked; 32 | if (chat.isPersisted) { 33 | updateChat({ id: chat.id, stream: $stream }); 34 | debug('Update Stream of Chat', $stream); 35 | } else { 36 | editChat({ stream: $stream }); 37 | debug('Edit Stream of Chat', $stream); 38 | } 39 | window.electron.ingestEvent([ 40 | { app: 'toggle-stream', stream: $stream ? 'on' : 'off' }, 41 | ]); 42 | }; 43 | 44 | const [open, setOpen] = useState(false); 45 | 46 | const handleOpenChange: PopoverProps['onOpenChange'] = (e, data) => 47 | setOpen(data.open || false); 48 | 49 | const renderLabel = () => { 50 | return ( 51 | 56 | ); 57 | }; 58 | 59 | useEffect(() => { 60 | setStream(ctx.isStream()); 61 | }, [ctx]); 62 | 63 | return ( 64 | 65 | 66 | 76 | 77 | 78 |
79 | 84 |
85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/renderer/pages/tool/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import Empty from 'renderer/components/Empty'; 4 | import TooltipIcon from 'renderer/components/TooltipIcon'; 5 | import useMCPStore, { IMCPServer } from 'stores/useMCPStore'; 6 | import Grid from './Grid'; 7 | import { MessageBar, MessageBarBody } from '@fluentui/react-components'; 8 | 9 | export default function Tools() { 10 | const { t } = useTranslation(); 11 | const [loading, setLoading] = useState(false); 12 | const remoteConfig = useMCPStore((state) => state.remoteConfig); 13 | const config = useMCPStore((state) => state.config); 14 | const activeServerNames = useMCPStore((state) => state.activeServerNames); 15 | 16 | const loadConfig = async () => { 17 | setLoading(true); 18 | try { 19 | await Promise.all([ 20 | useMCPStore.getState().fetchConfig(), 21 | useMCPStore.getState().getConfig(), 22 | useMCPStore.getState().getActiveServerNames(), 23 | ]); 24 | } catch (error) { 25 | console.error(error); 26 | } finally { 27 | setLoading(false); 28 | } 29 | }; 30 | 31 | const servers = useMemo(() => { 32 | const mergedServers = [...remoteConfig.servers]; 33 | config.servers.forEach((configServer) => { 34 | if (activeServerNames.includes(configServer.key)) { 35 | configServer.isActive = true; 36 | } else { 37 | configServer.isActive = false; 38 | } 39 | const index = mergedServers.findIndex( 40 | (remoteServer) => remoteServer.key === configServer.key 41 | ); 42 | if (index !== -1) { 43 | mergedServers[index] = configServer; 44 | } else { 45 | mergedServers.push(configServer); 46 | } 47 | }); 48 | return mergedServers; 49 | }, [remoteConfig, config, activeServerNames]); 50 | 51 | useEffect(() => { 52 | loadConfig(); 53 | }, []); 54 | 55 | return ( 56 |
57 |
58 |
59 |
60 |
61 |

{t('Common.Tools')}

62 |
63 |
64 | {t('Common.MCPServers')} 65 | 68 |
69 |
{t('Tools.PrerequisiteDescription')}
70 |
71 |
72 |
73 | {servers.length === 0 ? ( 74 | 75 | ) : ( 76 | 77 | )} 78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/renderer/components/layout/AppHeader.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { 3 | Button, 4 | Popover, 5 | PopoverSurface, 6 | PopoverTrigger, 7 | } from '@fluentui/react-components'; 8 | import { 9 | PanelLeftText24Filled, 10 | PanelLeftText24Regular, 11 | Search24Filled, 12 | Search24Regular, 13 | Wifi124Filled, 14 | Wifi124Regular, 15 | WifiOff24Filled, 16 | WifiOff24Regular, 17 | bundleIcon 18 | } from '@fluentui/react-icons'; 19 | import useOnlineStatus from 'hooks/useOnlineStatus'; 20 | import { useTranslation } from 'react-i18next'; 21 | import useAppearanceStore from '../../../stores/useAppearanceStore'; 22 | import './AppHeader.scss'; 23 | import SearchDialog from '../SearchDialog'; 24 | import TrafficLights from '../TrafficLights'; 25 | 26 | const PanelLeftIcon = bundleIcon(PanelLeftText24Filled, PanelLeftText24Regular); 27 | const SearchIcon = bundleIcon(Search24Filled, Search24Regular); 28 | const OnlineIcon = bundleIcon(Wifi124Filled, Wifi124Regular); 29 | const OfflineIcon = bundleIcon(WifiOff24Filled, WifiOff24Regular); 30 | 31 | export default function AppHeader() { 32 | const collapsed = useAppearanceStore((state) => state.sidebar.collapsed); 33 | const toggleSidebarVisibility = useAppearanceStore( 34 | (state) => state.toggleSidebarVisibility 35 | ); 36 | const { t } = useTranslation(); 37 | const [searchOpen, setSearchOpen] = useState(false); 38 | const NetworkStatusIcon = useOnlineStatus() ? ( 39 | 40 | 41 |
72 |
73 |
79 |
{NetworkStatusIcon}
80 |
81 | 82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/renderer/pages/tool/ParamsDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogTrigger, 4 | DialogSurface, 5 | DialogTitle, 6 | DialogContent, 7 | DialogBody, 8 | DialogActions, 9 | Button, 10 | Input, 11 | Label, 12 | } from '@fluentui/react-components'; 13 | import useToast from 'hooks/useToast'; 14 | import { useState } from 'react'; 15 | import { useTranslation } from 'react-i18next'; 16 | import { IMCPServerParameter } from 'utils/mcp'; 17 | 18 | export default function ParamsDialog({ 19 | title, 20 | open, 21 | setOpen, 22 | params, 23 | onSubmit, 24 | }: { 25 | title: string; 26 | open: boolean; 27 | setOpen: (open: boolean) => void; 28 | params: IMCPServerParameter[]; 29 | onSubmit: (values: { [key: string]: string }) => void; 30 | }) { 31 | const { t } = useTranslation(); 32 | const { notifyInfo } = useToast(); 33 | const [paramValues, setParamValues] = useState<{ [key: string]: string }>({}); 34 | 35 | const handleSubmit = (ev: React.FormEvent) => { 36 | ev.preventDefault(); 37 | for (const param of params) { 38 | if (!paramValues[param.name]) { 39 | notifyInfo(`${params} ${t('Common.Required')}`); 40 | return false; 41 | } 42 | } 43 | onSubmit(paramValues); 44 | setParamValues({}); 45 | setOpen(false); 46 | }; 47 | 48 | const setValue = (key: string, value: string) => { 49 | setParamValues((prev) => ({ ...prev, [key]: value })); 50 | }; 51 | 52 | return ( 53 | setOpen(data.open)}> 54 | 55 |
56 | 57 | {title} 58 | 59 |
60 | {t('MCP.ParameterDescription')} 61 |
62 |
63 |
{t('MCP.EditParamsTip')}
64 | {params.map((param) => ( 65 |
66 |
67 | 68 |
69 | 71 | setValue(param.name, ev.target?.value || '') 72 | } 73 | placeholder={param.description} 74 | className="w-full" 75 | /> 76 |
77 | ))} 78 |
79 |
80 | 81 | 82 | 83 | 84 | 87 | 88 |
89 |
90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/intellichat/readers/AnthropicReader.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { IChatResponseMessage } from 'intellichat/types'; 3 | import BaseReader from './BaseReader'; 4 | import { ITool } from './IChatReader'; 5 | 6 | const debug = Debug('5ire:intellichat:AnthropicReader'); 7 | 8 | export default class AnthropicReader extends BaseReader { 9 | 10 | protected parseReply(chunk: string): IChatResponseMessage { 11 | const data = JSON.parse(chunk); 12 | if (data.type === 'content_block_start') { 13 | if (data.content_block.type === 'tool_use') { 14 | return { 15 | toolCalls: [ 16 | { 17 | id: data.content_block.id, 18 | name: data.content_block.name, 19 | args:'', 20 | }, 21 | ], 22 | isEnd: false, 23 | }; 24 | } 25 | return { 26 | content: data.content_block.text, 27 | isEnd: false, 28 | }; 29 | } else if (data.type === 'content_block_delta') { 30 | if (data.delta.type === 'input_json_delta') { 31 | return { 32 | content: '', 33 | toolCalls: [ 34 | { 35 | args: data.delta.partial_json, 36 | index: 0, 37 | }, 38 | ], 39 | }; 40 | } 41 | return { 42 | content: data.delta.text, 43 | isEnd: false, 44 | }; 45 | } else if (data.type === 'message_start') { 46 | return { 47 | content: '', 48 | isEnd: false, 49 | inputTokens: data.message.usage.input_tokens, 50 | outputTokens: data.message.usage.output_tokens, 51 | }; 52 | } else if (data.type === 'message_delta') { 53 | return { 54 | content: '', 55 | isEnd: false, 56 | outputTokens: data.usage.output_tokens, 57 | }; 58 | } else if (data.type === 'message_stop') { 59 | return { 60 | content: '', 61 | isEnd: true, 62 | }; 63 | } else if (data.type === 'error') { 64 | return { 65 | content: '', 66 | error: { 67 | type: data.delta.type, 68 | message: data.delta.text, 69 | }, 70 | }; 71 | } else { 72 | console.warn('Unknown message type', data); 73 | return { 74 | content: '', 75 | isEnd: false, 76 | }; 77 | } 78 | } 79 | 80 | protected parseTools(respMsg: IChatResponseMessage): ITool | null { 81 | if (respMsg.toolCalls) { 82 | return { 83 | id: respMsg.toolCalls[0].id, 84 | name: respMsg.toolCalls[0].name, 85 | }; 86 | } 87 | return null; 88 | } 89 | 90 | protected parseToolArgs(respMsg: IChatResponseMessage): { 91 | index: number; 92 | args: string; 93 | } | null { 94 | debug('parseToolArgs', JSON.stringify(respMsg)); 95 | try { 96 | if (respMsg.isEnd || !respMsg.toolCalls) { 97 | return null; 98 | } 99 | return respMsg.toolCalls[0]; 100 | } catch (err) { 101 | console.error('parseToolArgs', err); 102 | } 103 | return null; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/providers/Google.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | export default { 4 | name: 'Google', 5 | apiBase: 'https://generativelanguage.googleapis.com', 6 | currency: 'USD', 7 | options: { 8 | apiBaseCustomizable: true, 9 | apiKeyCustomizable: true 10 | }, 11 | chat: { 12 | apiSchema: ['base', 'key', 'model'], 13 | presencePenalty: { min: -2, max: 2, default: 0 }, 14 | topP: { min: 0, max: 1, default: 1 }, 15 | temperature: { min: 0, max: 1, default: 0.9 }, 16 | options: { 17 | modelCustomizable: true, 18 | streamCustomizable: false, 19 | }, 20 | models: { 21 | 'gemini-1.5-pro': { 22 | name: 'gemini-1.5-pro', 23 | contextWindow: 1048576, 24 | maxTokens: 8192, 25 | inputPrice: 0.00035, 26 | outputPrice: 0.0105, 27 | jsonModelEnabled: true, 28 | toolEnabled: true, 29 | vision:{ 30 | enabled:true, 31 | }, 32 | description: `The multi-modal model from Google's Gemini family that balances model performance and speed.`, 33 | isDefault: true, 34 | group: 'Gemini', 35 | }, 36 | 'gemini-1.5-flash': { 37 | name: 'gemini-1.5-flash', 38 | contextWindow: 1048576, 39 | maxTokens: 8192, 40 | inputPrice: 0.00035, 41 | outputPrice: 0.00105, 42 | jsonModelEnabled: true, 43 | toolEnabled: true, 44 | vision:{ 45 | enabled:true, 46 | }, 47 | description: `Lightweight, fast and cost-efficient while featuring multimodal reasoning and a breakthrough long context window of up to one million tokens.`, 48 | group: 'Gemini', 49 | }, 50 | 'gemini-1.5-flash-8b': { 51 | name: 'gemini-1.5-flash-8b', 52 | contextWindow: 1048576, 53 | maxTokens: 8192, 54 | inputPrice: 0.0000375, 55 | outputPrice: 0.00015, 56 | jsonModelEnabled: true, 57 | toolEnabled: true, 58 | vision:{ 59 | enabled:true, 60 | }, 61 | description: `The Gemini 1.5 Flash-8B is a small model designed for tasks that require less intelligence.`, 62 | group: 'Gemini', 63 | }, 64 | 'gemini-2.0-flash-exp': { 65 | name: 'gemini-2.0-flash-exp', 66 | contextWindow: 1048576, 67 | maxTokens: 8192, 68 | inputPrice: 0.0000375, 69 | outputPrice: 0.00015, 70 | jsonModelEnabled: true, 71 | toolEnabled: true, 72 | vision:{ 73 | enabled:true, 74 | }, 75 | description: `Next generation features, superior speed, native tool use, and multimodal generation`, 76 | group: 'Gemini', 77 | }, 78 | 'gemini-exp-1206': { 79 | name: 'gemini-exp-1121', 80 | contextWindow: 1048576, 81 | maxTokens: 8192, 82 | inputPrice: 0.0000375, 83 | outputPrice: 0.00015, 84 | jsonModelEnabled: true, 85 | toolEnabled: true, 86 | vision:{ 87 | enabled:true, 88 | }, 89 | description: `Quality improvements, celebrate 1 year of Gemini`, 90 | group: 'Gemini', 91 | }, 92 | }, 93 | }, 94 | } as IServiceProvider; 95 | -------------------------------------------------------------------------------- /src/providers/Doubao.ts: -------------------------------------------------------------------------------- 1 | import { IServiceProvider } from './types'; 2 | 3 | export default { 4 | name: 'Doubao', 5 | apiBase: 'https://ark.cn-beijing.volces.com', 6 | currency: 'CNY', 7 | options: { 8 | apiBaseCustomizable: true, 9 | apiKeyCustomizable: true, 10 | }, 11 | chat: { 12 | docs: { 13 | deploymentId: '接入点名称, 类似 ep-20241101123241-24smv', 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 | model: '用于统计用量和控制上下文长度,请选择与部署一致的模型' 20 | }, 21 | apiSchema: ['base', 'key', 'model', 'deploymentId'], 22 | frequencyPenalty: { min: -2, max: 2, default: 0 }, 23 | topP: { min: 0, max: 1, default: 0.7 }, 24 | temperature: { min: 0, max: 1, default: 1}, 25 | options: { 26 | modelCustomizable: true, 27 | }, 28 | models: { 29 | 'doubao-pro-256k': { 30 | name: 'doubao-pro-256k', 31 | contextWindow: 256000, 32 | maxTokens: 4096, 33 | inputPrice: 0.005, 34 | outputPrice: 0.009, 35 | toolEnabled: true, 36 | group: 'Doubao-Pro', 37 | }, 38 | 'doubao-pro-128k': { 39 | name: 'doubao-pro-128k', 40 | contextWindow: 128000, 41 | maxTokens: 4096, 42 | inputPrice: 0.005, 43 | outputPrice: 0.009, 44 | isDefault: true, 45 | toolEnabled: true, 46 | group: 'Doubao-Pro', 47 | }, 48 | 'doubao-pro-32k': { 49 | name: 'gpt-4-turbo', 50 | contextWindow: 32000, 51 | maxTokens: 4096, 52 | inputPrice: 0.0008, 53 | outputPrice: 0.002, 54 | toolEnabled: true, 55 | group: 'Doubao-Pro', 56 | }, 57 | 'doubao-pro-4k': { 58 | name: 'doubao-pro-4k', 59 | contextWindow: 4000, 60 | maxTokens: 4096, 61 | inputPrice: 0.0008, 62 | outputPrice: 0.002, 63 | toolEnabled: true, 64 | group: 'Doubao-Pro', 65 | }, 66 | 'doubao-lite-128k': { 67 | name: 'doubao-lite-128k', 68 | contextWindow: 128000, 69 | maxTokens: 4096, 70 | inputPrice: 0.0008, 71 | outputPrice: 0.0010, 72 | toolEnabled: true, 73 | group: 'Doubao-Lite', 74 | }, 75 | 'doubao-lite-32k': { 76 | name: 'doubao-pro-4k', 77 | contextWindow: 32000, 78 | maxTokens: 4096, 79 | inputPrice: 0.0003, 80 | outputPrice: 0.0006, 81 | toolEnabled: true, 82 | group: 'Doubao-Lite', 83 | }, 84 | 'doubao-lite-4k': { 85 | name: 'doubao-pro-4k', 86 | contextWindow: 4000, 87 | maxTokens: 4096, 88 | inputPrice: 0.0003, 89 | outputPrice: 0.0006, 90 | toolEnabled: true, 91 | group: 'Doubao-Lite', 92 | }, 93 | }, 94 | }, 95 | } as IServiceProvider; 96 | -------------------------------------------------------------------------------- /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 | private downloads: { [key: string]: any } = {}; 9 | private onFailed: Function | undefined; 10 | constructor( 11 | win: any, 12 | { onStart, onCompleted, onFailed, onProgress } = { 13 | onStart: (fileName: string) => {}, 14 | onCompleted: (fileName: string, savePath: string) => {}, 15 | onFailed: (fileName: string, savePath: string, state: string) => {}, 16 | onProgress: (fileName: string, progress: number) => {}, 17 | } 18 | ) { 19 | this.win = win; 20 | this.onFailed = onFailed; 21 | 22 | this.win.webContents.session.on('will-download', (evt: any, item: any) => { 23 | const fileName = item.getFilename(); 24 | const savePath = path.join( 25 | app.getPath('userData'), 26 | 'downloads', 27 | item.getFilename() 28 | ); 29 | item.setSavePath(savePath); 30 | 31 | const download = this.downloads[item.getFilename()]; 32 | if (download?.cancelled) { 33 | delete this.downloads[item.getFilename()]; 34 | item.cancel(); 35 | try { 36 | fs.unlinkSync(savePath); 37 | } catch (e) { 38 | log.error('error deleting file', savePath, e); 39 | } 40 | onFailed && onFailed(fileName, savePath, 'cancelled'); 41 | return; 42 | } 43 | this.downloads[fileName] = item; 44 | onStart && onStart(fileName); 45 | 46 | item.on('updated', (_: any, state: string) => { 47 | if (state === 'progressing') { 48 | const progress = item.getReceivedBytes() / item.getTotalBytes(); 49 | onProgress && onProgress(fileName, progress); 50 | } 51 | }); 52 | 53 | item.once('done', (_: Electron.Event, state: string) => { 54 | log.debug(`Download ${state}`, fileName); 55 | if (state === 'completed') { 56 | onCompleted && onCompleted(fileName, savePath); 57 | } else { 58 | fs.unlink(savePath, (err) => { 59 | if (err) { 60 | log.warn('error deleting file', savePath, err); 61 | } 62 | }); 63 | onFailed && onFailed(fileName, savePath, state); 64 | } 65 | delete this.downloads[fileName]; 66 | }); 67 | }); 68 | } 69 | 70 | download(fileName: string, url: string) { 71 | this.downloads[fileName] = { pending: true }; 72 | this.win.webContents.session.downloadURL(url); 73 | } 74 | 75 | cancel(fileName: string) { 76 | let item = this.downloads[fileName]; 77 | if (!item) { 78 | this.downloads[fileName] = item = { pending: true }; 79 | } 80 | if (!item.pending) { 81 | log.debug(`Cancelling download ${fileName}`); 82 | item.cancel(); 83 | delete this.downloads[fileName]; 84 | this.onFailed && this.onFailed(fileName, 'cancelled'); 85 | fs.unlink(item.getSavePath(), (error) => { 86 | if (error) { 87 | log.warn('error deleting file', item.getSavePath(), error); 88 | } 89 | }); 90 | } else { 91 | item.cancelled = true; 92 | log.warn('Download not started yet, set to be cancelled on start'); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/providers/types.ts: -------------------------------------------------------------------------------- 1 | export type ProviderType = 2 | | 'OpenAI' 3 | | 'Google' 4 | | 'Azure' 5 | | 'Baidu' 6 | | 'Anthropic' 7 | | 'Moonshot' 8 | | 'DeepSeek' 9 | | 'Ollama' 10 | | 'ChatBro' 11 | | '5ire' 12 | | 'Doubao' 13 | | 'Grok' 14 | 15 | export interface INumberRange { 16 | min: number; 17 | max: number; 18 | default: number|null; 19 | interval?: { 20 | leftOpen: boolean; 21 | rightOpen: boolean; 22 | }; 23 | } 24 | 25 | export type ChatModelGroup = 26 | | 'o1' 27 | | 'GPT-3.5' 28 | | 'GPT-4' 29 | | 'Gemini' 30 | | 'Grok' 31 | | 'DeepSeek' 32 | | 'ERNIE' 33 | | 'Moonshot' 34 | | 'Claude-3' 35 | | 'Claude-3.5' 36 | | 'Doubao-Pro' 37 | | 'Doubao-Lite' 38 | | 'Open Source'; 39 | 40 | 41 | export interface IChatModelVision{ 42 | enabled: boolean; 43 | allowUrl?: boolean; 44 | allowBase64?: boolean; 45 | allowedMimeTypes?: string[]; 46 | } 47 | export interface IChatModel { 48 | label?: string; 49 | name: string; 50 | description?: string | null; 51 | maxTokens?: number | null; 52 | contextWindow: number | null; 53 | isDefault?: boolean; 54 | inputPrice: number; 55 | outputPrice: number; 56 | jsonModelEnabled?: boolean; 57 | toolEnabled?: boolean; 58 | vision?: IChatModelVision; 59 | endpoint?: string; 60 | group: ChatModelGroup; 61 | } 62 | 63 | export interface IChatConfig { 64 | apiSchema: string[]; 65 | /** 66 | * Positive values penalize new tokens based on whether they appear 67 | * in the text so far, increasing the model's likelihood to talk about new topics. 68 | */ 69 | presencePenalty?: INumberRange; 70 | /** 71 | * An alternative to sampling with temperature, called nucleus sampling, 72 | * where the model considers the results of the tokens with top_p probability mass. 73 | */ 74 | topP: INumberRange; 75 | /** 76 | * What sampling temperature to use, 77 | * Higher values will make the output more random, 78 | * while lower values make it more focused and deterministic. 79 | */ 80 | temperature: INumberRange; 81 | models: { [key: string]: IChatModel }; 82 | docs?: { [key: string]: string }; 83 | placeholders?: { [key: string]: string }; 84 | options: { 85 | modelCustomizable?: boolean; 86 | streamCustomizable?: boolean; 87 | }; 88 | } 89 | 90 | export interface IEmbeddingModel { 91 | name: string; 92 | price: number; 93 | dimension?: number; 94 | description?: string; 95 | maxTokens?: number; 96 | maxChars?: number; 97 | isDefault?: boolean; 98 | } 99 | 100 | export interface IEmbeddingConfig { 101 | apiSchema: string[]; 102 | docs?: { [key: string]: string }; 103 | placeholders?: { [key: string]: string }; 104 | models: { [key: string]: IEmbeddingModel }; 105 | options?: { 106 | modelCustomizable?: boolean; 107 | }; 108 | } 109 | 110 | export interface IServiceProvider { 111 | name: ProviderType; 112 | description?: string; 113 | disabled?: boolean; 114 | isPremium?: boolean; 115 | apiBase: string; 116 | apiKey?: string; 117 | currency: 'USD' | 'CNY'; 118 | options: { 119 | apiBaseCustomizable?: boolean; 120 | apiKeyCustomizable?: boolean; 121 | }; 122 | chat: IChatConfig; 123 | embedding?: IEmbeddingConfig; 124 | } 125 | -------------------------------------------------------------------------------- /src/utils/token.ts: -------------------------------------------------------------------------------- 1 | import { 2 | encoding_for_model as encodingForModel, 3 | TiktokenModel, 4 | Tiktoken, 5 | } from 'tiktoken'; 6 | import { get_encoding as getEncoding } from 'tiktoken/init'; 7 | import { IChatRequestMessage } from 'intellichat/types'; 8 | 9 | let llama3Tokenizer: any; 10 | let llamaTokenizer: any; 11 | 12 | (async () => { 13 | llama3Tokenizer = (await import('llama3-tokenizer-js')).default; 14 | llamaTokenizer = (await import('llama-tokenizer-js')).default; 15 | })(); 16 | 17 | export function countGPTTokens(messages: IChatRequestMessage[], model: string) { 18 | let _model = model; 19 | if (model.startsWith('gpt-3.5') || model.startsWith('gpt-35')) { 20 | _model = 'gpt-3.5-turbo-0613'; 21 | } else if (model.startsWith('gpt-4')) { 22 | _model = 'gpt-4-0613'; 23 | } 24 | let encoding: Tiktoken; 25 | try { 26 | encoding = encodingForModel(_model as TiktokenModel); 27 | } catch (err) { 28 | console.warn('Model not found. Using cl100k_base encoding.'); 29 | encoding = getEncoding('cl100k_base'); 30 | } 31 | let tokensPerMessage = 3; 32 | let tokensPerName = 1; 33 | let numTokens = 0; 34 | 35 | messages.forEach((msg: any) => { 36 | numTokens += tokensPerMessage; 37 | Object.keys(msg).forEach((key: string) => { 38 | numTokens += encoding.encode(msg[key] as string).length; 39 | if (key === 'name') { 40 | numTokens += tokensPerName; 41 | } 42 | }); 43 | }); 44 | numTokens += 3; // For assistant prompt 45 | return numTokens; 46 | } 47 | 48 | export async function countTokensOfGemini( 49 | messages: IChatRequestMessage[], 50 | apiBase: string, 51 | apiKey: string, 52 | model: string 53 | ) { 54 | const response = await fetch( 55 | `${apiBase}/v1beta/models/${model}:countTokens?key=${apiKey}`, 56 | { 57 | method: 'POST', 58 | headers: { 59 | 'Content-Type': 'application/json', 60 | }, 61 | body: JSON.stringify({ contents: messages }), 62 | } 63 | ); 64 | const data = await response.json(); 65 | return data.totalTokens; 66 | } 67 | 68 | export async function countTokensOfMoonshot( 69 | messages: IChatRequestMessage[], 70 | apiBase: string, 71 | apiKey: string, 72 | model: string 73 | ) { 74 | const response = await fetch( 75 | `${apiBase}/v1/tokenizers/estimate-token-count`, 76 | { 77 | method: 'POST', 78 | headers: { 79 | Authorization: 'Bearer ' + apiKey, 80 | 'Content-Type': 'application/json', 81 | }, 82 | body: JSON.stringify({ model, messages }), 83 | } 84 | ); 85 | const json = await response.json(); 86 | return json.data.total_tokens; 87 | } 88 | 89 | export async function countTokenOfLlama( 90 | messages: IChatRequestMessage[], 91 | model: string 92 | ) { 93 | const tokensPerMessage = 3; 94 | const tokensPerName = 1; 95 | let numTokens = 0; 96 | let tokenizer = model.startsWith('llama3') ? llama3Tokenizer : llamaTokenizer; 97 | messages.forEach((msg: any) => { 98 | numTokens += tokensPerMessage; 99 | Object.keys(msg).forEach((key: string) => { 100 | numTokens += tokenizer.encode(msg[key] as string).length; 101 | if (key === 'name') { 102 | numTokens += tokensPerName; 103 | } 104 | }); 105 | }); 106 | return numTokens; 107 | } 108 | -------------------------------------------------------------------------------- /src/renderer/pages/chat/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Button, Tooltip } from '@fluentui/react-components'; 3 | import { 4 | MoreHorizontal24Regular, 5 | MoreHorizontal24Filled, 6 | FilterDismiss24Regular, 7 | Delete24Regular, 8 | Delete24Filled, 9 | bundleIcon 10 | } from '@fluentui/react-icons'; 11 | import useAppearanceStore from 'stores/useAppearanceStore'; 12 | import useChatStore from 'stores/useChatStore'; 13 | import { useTranslation } from 'react-i18next'; 14 | import useChatContext from 'hooks/useChatContext'; 15 | import ChatSettingsDrawer from './ChatSettingsDrawer'; 16 | import ConfirmDialog from 'renderer/components/ConfirmDialog'; 17 | 18 | import { tempChatId } from 'consts'; 19 | import useNav from 'hooks/useNav'; 20 | import useToast from 'hooks/useToast'; 21 | 22 | const DeleteIcon = bundleIcon(Delete24Filled,Delete24Regular); 23 | const MoreHorizontalIcon = bundleIcon(MoreHorizontal24Filled,MoreHorizontal24Regular); 24 | 25 | export default function Header() { 26 | const { t } = useTranslation(); 27 | const { notifySuccess } = useToast(); 28 | const navigate = useNav(); 29 | 30 | const activeChat = useChatContext().getActiveChat(); 31 | const collapsed = useAppearanceStore((state) => state.sidebar.collapsed); 32 | const [drawerOpen, setDrawerOpen] = useState(false); 33 | 34 | const [delConfirmDialogOpen, setDelConfirmDialogOpen] = useState(false); 35 | const deleteChat = useChatStore((state) => state.deleteChat); 36 | const onDeleteChat = async () => { 37 | await deleteChat(); 38 | navigate(`/chats/${tempChatId}`); 39 | notifySuccess(t('Chat.Notification.Deleted')); 40 | }; 41 | 42 | const getKeyword = useChatStore((state) => state.getKeyword); 43 | const setKeyword = useChatStore((state) => state.setKeyword); 44 | 45 | const keyword = activeChat.isPersisted ? getKeyword(activeChat?.id) : null; 46 | return ( 47 |
54 |
55 | {activeChat.isPersisted ? ( 56 | <> 57 |
79 | 80 | 89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/renderer/pages/chat/Chat.scss: -------------------------------------------------------------------------------- 1 | .chat-header { 2 | right: 0; 3 | top: 0; 4 | z-index: 2; 5 | -webkit-app-region: drag; 6 | } 7 | .message { 8 | text-align: left; 9 | font-family: Barlow, -apple-system, BlinkMacSystemFont, PingFang SC, Hiragino Sans GB, 10 | Roboto, helvetica neue, helvetica, segoe ui, Arial, sans-serif; 11 | } 12 | .message .avatar { 13 | width: 32px; 14 | height: 32px; 15 | border-radius: 32px; 16 | } 17 | .message p:not(:first-of-type) { 18 | margin-top: 12px; 19 | } 20 | 21 | .message p { 22 | margin-bottom: 3px; 23 | line-height: 1.8; 24 | } 25 | 26 | .message ul{ 27 | margin-bottom: 12px; 28 | } 29 | .message-cited-files{ 30 | margin-left: 40px; 31 | } 32 | .message-toolbar { 33 | margin-left: 40px; 34 | margin-top: 7px; 35 | margin-bottom: 10px; 36 | --tw-bg-opacity: 1; 37 | background-color: rgba(var(--color-bg-sidebar), var(--tw-bg-opacity)); 38 | visibility: hidden; 39 | } 40 | .message:hover .message-toolbar { 41 | visibility: visible; 42 | } 43 | .message a{ 44 | text-decoration: underline; 45 | } 46 | .message li{ 47 | padding:2px 0; 48 | } 49 | .msg-prompt .avatar { 50 | background-image: linear-gradient(to right, #fa709a 0%, #fee140 100%); 51 | } 52 | 53 | .msg-reply .avatar { 54 | background-image: linear-gradient(to top, #d9afd9 0%, #97d9e1 100%); 55 | } 56 | .msg-reply ul, .msg-reply ol{ 57 | margin-bottom:15px; 58 | } 59 | 60 | .editor { 61 | height: 100%; 62 | } 63 | .editor img{ 64 | width:300px; 65 | } 66 | .editor-loading-mask { 67 | z-index: 10; 68 | left: 0; 69 | right: 0; 70 | top: 0; 71 | bottom: 0; 72 | --tw-bg-opacity: 0.8; 73 | background-color: rgba(var(--color-bg-surface-2), var(--tw-bg-opacity)); 74 | } 75 | .editor-toolbar { 76 | .fui-Button__icon { 77 | margin-right: 0!important; 78 | margin-left:0!important; 79 | } 80 | button{ 81 | font-family: Barlow; 82 | } 83 | } 84 | .ellipsis-loader { 85 | font-family: -apple-system, BlinkMacSystemFont, PingFang SC, Hiragino Sans GB, 86 | Microsoft YaHei, '微软雅黑', helvetica neue, helvetica, ubuntu, roboto, noto, 87 | segoe ui, Arial, sans-serif; 88 | } 89 | .ellipsis-loader:after { 90 | overflow: hidden; 91 | display: inline-block; 92 | vertical-align: bottom; 93 | -webkit-animation: ellipsis steps(4, end) 600ms infinite; 94 | animation: ellipsis steps(4, end) 600ms infinite; 95 | content: '\2026'; /* ascii code for the ellipsis character */ 96 | width: 0px; 97 | } 98 | 99 | @keyframes ellipsis { 100 | to { 101 | width: 1.25em; 102 | } 103 | } 104 | 105 | @-webkit-keyframes ellipsis { 106 | to { 107 | width: 1.25em; 108 | } 109 | } 110 | 111 | .blinking-cursor { 112 | -webkit-animation: .6s blink infinite; 113 | animation: .6s blink infinite; 114 | transform: scale(1); 115 | display: inline-block; 116 | width:14px; 117 | height:14px; 118 | border-radius: 14px; 119 | margin-left: 5px; 120 | margin-bottom: -3px; 121 | background-color: rgba(var(--color-text-secondary), 1); 122 | } 123 | 124 | @keyframes blink { 125 | from, to { 126 | opacity: 1; 127 | transform: scale(1); 128 | } 129 | 50% { 130 | opacity: 0.8; 131 | transform: scale(1.2); 132 | } 133 | } 134 | 135 | @-webkit-keyframes blink { 136 | from, to { 137 | opacity: 1; 138 | transform: scale(1); 139 | } 140 | 50% { 141 | opacity: 0.8; 142 | transform: scale(1.2); 143 | } 144 | } 145 | --------------------------------------------------------------------------------