├── .prettierignore ├── src-tauri ├── build.rs ├── icons │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 32x32.png │ ├── icon.icns │ ├── StoreLogo.png │ ├── icon-1024.png │ ├── icon-raw.png │ ├── 128x128@2x.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ └── Square89x89Logo.png ├── .gitignore ├── src │ └── main.rs ├── Cargo.toml └── tauri.conf.json ├── src ├── react-app-env.d.ts ├── assets │ └── icon.png ├── setupTests.ts ├── styles │ ├── App.scss │ ├── index.css │ ├── index.scss │ └── Block.scss ├── reportWebVitals.ts ├── components │ ├── SortableItem.tsx │ ├── StyledMenu.tsx │ ├── SponsorChip.tsx │ ├── InputBox.tsx │ ├── SessionItem.tsx │ └── MessageBox.tsx ├── hooks │ └── useEvent.ts ├── i18n │ ├── index.ts │ └── locales │ │ ├── zh-Hant │ │ └── translation.json │ │ ├── zh-Hans │ │ └── translation.json │ │ ├── jp │ │ └── translation.json │ │ ├── en │ │ └── translation.json │ │ └── fr │ │ └── translation.json ├── packages │ ├── prompts.ts │ ├── remote.ts │ ├── runtime.ts │ ├── utils.ts │ ├── llm.ts │ └── defaults.ts ├── theme │ ├── index.ts │ ├── ThemeChangeIcon.tsx │ └── ThemeSwitcher.tsx ├── dialogs │ ├── CleanDialog.tsx │ ├── ChatConfigDialog.tsx │ ├── AboutDialog.tsx │ └── SettingDialog.tsx ├── index.tsx └── stores │ ├── types.ts │ └── store.ts ├── doc ├── demo.png ├── icon.png ├── mac.png ├── demo2.png ├── demo3.gif ├── linux.png ├── ali_pay.PNG ├── android.png ├── snapshot2.png ├── snapshot4.png ├── windows.png ├── app_store.webp ├── wechat_pay.JPG ├── demo_desktop_1.jpg ├── demo_desktop_2.jpg ├── demo_desktop_3.jpg └── google_play.webp ├── .eslintignore ├── team-sharing ├── demo_http.png ├── demo_https.png ├── Caddyfile ├── Dockerfile ├── main.sh └── README.md ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md ├── FUNDING.yml └── workflows │ └── publish.yml ├── Makefile ├── tsconfig.json ├── .eslintrc.js ├── public └── index.html ├── .gitignore ├── FAQ.md ├── package.json ├── script └── release-update.js ├── README_cn.md └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | .github 2 | .webpack 3 | build 4 | src-tauri 5 | tmp -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /doc/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/demo.png -------------------------------------------------------------------------------- /doc/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/icon.png -------------------------------------------------------------------------------- /doc/mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/mac.png -------------------------------------------------------------------------------- /doc/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/demo2.png -------------------------------------------------------------------------------- /doc/demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/demo3.gif -------------------------------------------------------------------------------- /doc/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/linux.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .github 2 | .webpack 3 | build 4 | src-tauri 5 | tmp 6 | 7 | *.js -------------------------------------------------------------------------------- /doc/ali_pay.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/ali_pay.PNG -------------------------------------------------------------------------------- /doc/android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/android.png -------------------------------------------------------------------------------- /doc/snapshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/snapshot2.png -------------------------------------------------------------------------------- /doc/snapshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/snapshot4.png -------------------------------------------------------------------------------- /doc/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/windows.png -------------------------------------------------------------------------------- /doc/app_store.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/app_store.webp -------------------------------------------------------------------------------- /doc/wechat_pay.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/wechat_pay.JPG -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src/assets/icon.png -------------------------------------------------------------------------------- /doc/demo_desktop_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/demo_desktop_1.jpg -------------------------------------------------------------------------------- /doc/demo_desktop_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/demo_desktop_2.jpg -------------------------------------------------------------------------------- /doc/demo_desktop_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/demo_desktop_3.jpg -------------------------------------------------------------------------------- /doc/google_play.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/doc/google_play.webp -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /team-sharing/demo_http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/team-sharing/demo_http.png -------------------------------------------------------------------------------- /team-sharing/demo_https.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/team-sharing/demo_https.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/icon-1024.png -------------------------------------------------------------------------------- /src-tauri/icons/icon-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/icon-raw.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suge8/chatbox/main/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. / 其他建设性意见与讨论 4 | title: '[Other]' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /team-sharing/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | reverse_proxy https://api.openai.com { 3 | header_up Host {http.reverse_proxy.upstream.hostport} 4 | header_up Authorization "Bearer " 5 | } 6 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION ?= 0.1.0 2 | 3 | change-version: 4 | sed -i -e "s/\"version\": \".*\"/\"version\": \"$(VERSION)\"/" src-tauri/tauri.conf.json 5 | sed -i -e "s/\"version\": \".*\"/\"version\": \"$(VERSION)\"/" package.json 6 | -------------------------------------------------------------------------------- /team-sharing/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM caddy:2.7.3 2 | 3 | COPY ./Caddyfile /etc/caddy/Caddyfile 4 | COPY ./main.sh /usr/src/www/main.sh 5 | 6 | RUN chmod +x /usr/src/www/main.sh 7 | 8 | ENTRYPOINT ["sh", "-c", "/usr/src/www/main.sh"] 9 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | all(not(debug_assertions), target_os = "windows"), 3 | windows_subsystem = "windows" 4 | )] 5 | 6 | fn main() { 7 | tauri::Builder::default() 8 | .plugin(tauri_plugin_store::Builder::default().build()) 9 | .run(tauri::generate_context!()) 10 | .expect("error while running tauri application"); 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | box-sizing: border-box; 3 | width: 100vw; 4 | height: 100vh; 5 | padding: 0 20px; 6 | } 7 | 8 | html[data-theme='light'] { 9 | .ToolBar { 10 | background-color: #fff; 11 | } 12 | } 13 | 14 | html[data-theme='dark'] { 15 | .ToolBar { 16 | background-color: rgb(40, 40, 40); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /team-sharing/main.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | if [ -z "$HOST" ] 4 | then 5 | HOST=":80" 6 | fi 7 | 8 | sed "s//$HOST/g" /etc/caddy/Caddyfile > /etc/caddy/Caddyfile.tmp 9 | mv /etc/caddy/Caddyfile.tmp /etc/caddy/Caddyfile 10 | 11 | sed "s//$KEY/g" /etc/caddy/Caddyfile > /etc/caddy/Caddyfile.tmp 12 | mv /etc/caddy/Caddyfile.tmp /etc/caddy/Caddyfile 13 | 14 | 15 | caddy run --config /etc/caddy/Caddyfile --adapter caddyfile -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 4 | 'Droid Sans', 'Helvetica Neue', sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | overscroll-behavior: none; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 12 | } 13 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals' 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry) 7 | getFID(onPerfEntry) 8 | getFCP(onPerfEntry) 9 | getLCP(onPerfEntry) 10 | getTTFB(onPerfEntry) 11 | }) 12 | } 13 | } 14 | 15 | export default reportWebVitals 16 | -------------------------------------------------------------------------------- /src/components/SortableItem.tsx: -------------------------------------------------------------------------------- 1 | import { useSortable } from '@dnd-kit/sortable' 2 | import { CSS } from '@dnd-kit/utilities' 3 | 4 | interface Props { 5 | id: string 6 | children?: React.ReactNode 7 | } 8 | 9 | export function SortableItem(props: Props) { 10 | const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props.id }) 11 | 12 | const style = { 13 | transform: CSS.Transform.toString(transform), 14 | transition, 15 | } 16 | 17 | return ( 18 |
19 | {props.children} 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useEvent.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useLayoutEffect, useRef } from 'react' 2 | 3 | // see https://github.com/reactjs/rfcs/pull/220 4 | export default function useEvent any>(handler: F) { 5 | const handlerRef = useRef(null) 6 | // In a real implementation, this would run before layout effects 7 | useLayoutEffect(() => { 8 | handlerRef.current = handler as any 9 | }) 10 | return useCallback((...args: Parameters): ReturnType => { 11 | // In a real implementation, this would throw if called during render 12 | const fn = handlerRef.current as any 13 | return fn(...args) 14 | }, []) 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Bin-Huang 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | 4 | import en from './locales/en/translation.json' 5 | import zhHans from './locales/zh-Hans/translation.json' 6 | import zhHant from './locales/zh-Hant/translation.json' 7 | import jp from './locales/jp/translation.json' 8 | 9 | const resources = { 10 | 'zh-Hans': { 11 | translation: zhHans, 12 | }, 13 | 'zh-Hant': { 14 | translation: zhHant, 15 | }, 16 | en: { 17 | translation: en, 18 | }, 19 | jp: { 20 | translation: jp, 21 | }, 22 | } 23 | 24 | i18n.use(initReactI18next).init({ 25 | resources, 26 | fallbackLng: 'en', 27 | 28 | interpolation: { 29 | escapeValue: false, 30 | }, 31 | 32 | detection: { 33 | caches: [], 34 | }, 35 | }) 36 | 37 | export default i18n 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project / 新功能新特性的想法(提交前请检查是否有重复 issues) 4 | title: '[Feature]' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Problem Description** 10 | Please describe the issue or difficulty you are experiencing and why it makes using the software difficult or frustrating. 11 | 12 | **Proposed Solution** 13 | Please provide a clear and concise description of what you would like to see in terms of a function or solution. 14 | 15 | **Additional Context** 16 | Please provide any additional context or information that would help better understanding your feature request, such as screenshots, examples, or use cases. 17 | 18 | --- 19 | 20 | **问题描述** 21 | 请描述您遇到的问题或难题,以及为什么这使得使用软件变得困难或令人沮丧。 22 | 23 | **解决思路** 24 | 请提供一个清晰、简洁的描述,说明您希望看到的功能或解决方案。 25 | 26 | **附加上下文** 27 | 请提供任何其他上下文或信息,以便更好地理解您的功能请求,例如截图、示例或用例。 28 | -------------------------------------------------------------------------------- /src/packages/prompts.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../stores/types' 2 | 3 | export function nameConversation(msgs: Message[]): Message[] { 4 | const format = (msgs: string[]) => msgs.map((msg) => msg).join('\n\n---------\n\n') 5 | return [ 6 | { 7 | id: '1', 8 | role: 'system', 9 | content: `Name the conversation based on the chat records. 10 | Please provide a concise name, within 10 characters and without quotation marks. 11 | Please use the speak language in the conversation. 12 | You only need to answer with the name. 13 | The following is the conversation: 14 | 15 | \`\`\` 16 | ${format(msgs.map((msg) => msg.content))} 17 | \`\`\` 18 | 19 | Please provide a concise name, within 10 characters and without quotation marks. 20 | Please use the speak language in the conversation. 21 | You only need to answer with the name. 22 | The conversation is named:`, 23 | }, 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/packages/remote.ts: -------------------------------------------------------------------------------- 1 | import { Config, SponsorAboutBanner, SponsorAd } from '../stores/types' 2 | 3 | const releaseHost = 'https://releases.chatboxai.app' 4 | 5 | export async function checkNeedUpdate(version: string, os: string, config: Config): Promise { 6 | const res = await fetch(`${releaseHost}/chatbox_need_update/${version}`, { 7 | method: 'POST', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | body: JSON.stringify({ 12 | uuid: config.uuid, 13 | os: os, 14 | }), 15 | }) 16 | const json = await res.json() 17 | return !!json['need_update'] 18 | } 19 | 20 | export async function getSponsorAd(): Promise { 21 | const res = await fetch(`${releaseHost}/sponsor_ad`) 22 | const json = await res.json() 23 | return json['data'] || null 24 | } 25 | 26 | export async function listSponsorAboutBanner(): Promise { 27 | const res = await fetch(`${releaseHost}/sponsor_about_banner`) 28 | const json = await res.json() 29 | return json['data'] 30 | } 31 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "0.1.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "" 7 | repository = "" 8 | default-run = "app" 9 | edition = "2021" 10 | rust-version = "1.59" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.2.1", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.2.4", features = ["app-all", "dialog-all", "fs-all", "http-all", "os-all", "shell-open", "updater", "window-all"] } 21 | tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } 22 | 23 | [features] 24 | # by default Tauri runs in production mode 25 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 26 | default = [ "custom-protocol" ] 27 | # this feature is used for production builds where `devPath` points to the filesystem 28 | # DO NOT remove this 29 | custom-protocol = [ "tauri/custom-protocol" ] 30 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { PaletteMode } from '@mui/material' 2 | import { ThemeOptions } from '@mui/material/styles' 3 | 4 | export enum ThemeMode { 5 | Dark, 6 | Light, 7 | System, 8 | } 9 | 10 | export type RealThemeMode = Exclude 11 | 12 | const ThemeModeMapPaletteMode: Record = { 13 | [ThemeMode.Dark]: 'dark', 14 | [ThemeMode.Light]: 'light', 15 | } 16 | 17 | export function fetchThemeDesign(mode: RealThemeMode, fontSize: number): ThemeOptions { 18 | return { 19 | palette: { 20 | mode: ThemeModeMapPaletteMode[mode], 21 | ...(mode === ThemeMode.Light 22 | ? {} 23 | : { 24 | background: { 25 | default: 'rgb(40, 40, 40)', 26 | paper: 'rgb(40, 40, 40)', 27 | }, 28 | }), 29 | }, 30 | typography: { 31 | // In Chinese and Japanese the characters are usually larger, 32 | // so a smaller fontsize may be appropriate. 33 | fontSize, 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | 6 | // plugins: ['@typescript-eslint'], 7 | rules: { 8 | // A temporary hack related to IDE not resolving correct package.json 9 | 'import/no-extraneous-dependencies': 'off', 10 | 'react/react-in-jsx-scope': 'off', 11 | 'react/jsx-filename-extension': 'off', 12 | 'import/extensions': 'off', 13 | 'import/no-unresolved': 'off', 14 | 'import/no-import-module-exports': 'off', 15 | 'no-shadow': 'off', 16 | '@typescript-eslint/no-shadow': 'off', 17 | 'no-unused-vars': 'off', 18 | '@typescript-eslint/no-unused-vars': 'off', 19 | '@typescript-eslint/no-extra-semi': 'off', 20 | }, 21 | parserOptions: { 22 | ecmaVersion: 2020, 23 | sourceType: 'module', 24 | // project: './tsconfig.json', 25 | tsconfigRootDir: __dirname, 26 | createDefaultProgram: true, 27 | }, 28 | settings: { 29 | 'import/parsers': { 30 | '@typescript-eslint/parser': ['.ts', '.tsx'], 31 | }, 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /src/theme/ThemeChangeIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonGroup } from '@mui/material' 2 | import { ThemeMode } from '.' 3 | import BrightnessMediumIcon from '@mui/icons-material/BrightnessMedium' 4 | import DarkModeIcon from '@mui/icons-material/DarkMode' 5 | import WbSunnyIcon from '@mui/icons-material/WbSunny' 6 | 7 | const ThemeModeMapIcon: Record = { 8 | [ThemeMode.Dark]: , 9 | [ThemeMode.Light]: , 10 | [ThemeMode.System]: , 11 | } 12 | 13 | const menuItems = [ThemeMode.System, ThemeMode.Dark, ThemeMode.Light] 14 | 15 | interface ThemeChangeIconProps { 16 | onChange: (value: ThemeMode) => void 17 | value: ThemeMode 18 | } 19 | 20 | export default function ThemeChangeButton(props: ThemeChangeIconProps) { 21 | return ( 22 | 23 | {menuItems.map((item) => ( 24 | 31 | ))} 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/dialogs/CleanDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Dialog, DialogContent, DialogActions, DialogTitle, DialogContentText } from '@mui/material' 2 | import { Session } from '../stores/types' 3 | import { useTranslation } from 'react-i18next' 4 | 5 | interface Props { 6 | open: boolean 7 | session: Session 8 | save(session: Session): void 9 | close(): void 10 | } 11 | 12 | export default function CleanDialog(props: Props) { 13 | const { t } = useTranslation() 14 | const clean = () => { 15 | const messages = props.session.messages.filter((msg) => msg.role === 'system') 16 | props.save({ ...props.session, messages }) 17 | } 18 | return ( 19 | 20 | {t('clean')} 21 | 22 | {t('delete confirmation', { sessionName: props.session.name })} 23 | 24 | 25 | 26 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/components/StyledMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Menu, MenuProps } from '@mui/material' 2 | import { styled, alpha } from '@mui/material/styles' 3 | 4 | const StyledMenu = styled((props: MenuProps) => ( 5 | 17 | ))(({ theme }) => ({ 18 | '& .MuiPaper-root': { 19 | borderRadius: 6, 20 | marginTop: theme.spacing(1), 21 | minWidth: 140, 22 | color: theme.palette.mode === 'light' ? 'rgb(55, 65, 81)' : theme.palette.grey[300], 23 | boxShadow: 24 | 'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px', 25 | '& .MuiMenu-list': { 26 | padding: '4px 0', 27 | }, 28 | '& .MuiMenuItem-root': { 29 | '& .MuiSvgIcon-root': { 30 | color: theme.palette.text.secondary, 31 | marginRight: theme.spacing(1.5), 32 | }, 33 | '&:active': { 34 | backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity), 35 | }, 36 | }, 37 | }, 38 | })) 39 | 40 | export default StyledMenu 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve / BUG 反馈(提交前请搜索是否存在重复issues) 4 | title: '[BUG]' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Bug Description** 10 | Please provide a clear and concise description of what the bug is. 11 | 12 | **Steps to Reproduce** 13 | Please provide the steps to reproduce the bug: 14 | 15 | 1. Go to "..." 16 | 2. Click on "..." 17 | 3. Scroll down to "..." 18 | 4. Observe the bug. 19 | 20 | **Expected Results** 21 | Please provide a clear and concise description of what you expected to happen. 22 | 23 | **Actual Results** 24 | Please provide a clear and concise description of what actually happened. 25 | 26 | **Screenshots** 27 | If possible, please add screenshots to help explain the issue. 28 | 29 | **Desktop (please complete the following information):** 30 | 31 | - Operating System: [e.g. macOS] 32 | - Application Version: [e.g. 2.0.1] 33 | 34 | **Additional Context** 35 | Please provide any additional context about the issue, such as interactions with other software or applications. 36 | 37 | --- 38 | 39 | **Bug 描述** 40 | 清晰简洁地描述这个 bug 是什么。 41 | 42 | **重现步骤** 43 | 请提供能够让我们重现这个 bug 的步骤: 44 | 45 | 1. 前往 "......" 46 | 2. 点击 "......" 47 | 3. 滚动到 "......" 48 | 4. 发现了这个 bug。 49 | 50 | **期望结果** 51 | 请清晰简洁地描述预期的行为。 52 | 53 | **实际结果** 54 | 请清晰简洁地描述实际的行为。 55 | 56 | **截图** 57 | 如果可行,添加截图以帮助解释问题。 58 | 59 | **桌面端(请填写以下信息):** 60 | 61 | - 操作系统:[例如 macOS] 62 | - 应用程序版本:[例如 2.0.1] 63 | 64 | **其他上下文** 65 | 在这里提供关于问题的任何其他上下文,例如与其他软件或应用程序的交互等。 66 | -------------------------------------------------------------------------------- /src/components/SponsorChip.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { Chip } from '@mui/material' 3 | import { SponsorAd } from '../stores/types' 4 | import CampaignOutlinedIcon from '@mui/icons-material/CampaignOutlined' 5 | import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined' 6 | import * as remote from '../packages/remote' 7 | import * as runtime from '../packages/runtime' 8 | 9 | export interface Props { 10 | sessionId: string 11 | } 12 | 13 | export default function SponsorChip(props: Props) { 14 | const [showSponsorAD, setShowSponsorAD] = useState(true) 15 | const [sponsorAD, setSponsorAD] = useState(null) 16 | useEffect(() => { 17 | ;(async () => { 18 | const ad = await remote.getSponsorAd() 19 | if (ad) { 20 | setSponsorAD(ad) 21 | } 22 | })() 23 | }, [props.sessionId]) 24 | if (!showSponsorAD || !sponsorAD) { 25 | return <> 26 | } 27 | return ( 28 | } 42 | deleteIcon={} 43 | onDelete={() => setShowSponsorAD(false)} 44 | onClick={() => runtime.openLink(sponsorAD.url)} 45 | label={sponsorAD.text} 46 | /> 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 26 | Chatbox 27 | 28 | 29 |
30 | 40 | 41 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import reportWebVitals from './reportWebVitals' 5 | // import ReactGA from 'react-ga4' 6 | // import * as store from './stores/store' 7 | // import * as runtime from './packages/runtime' 8 | import './i18n' 9 | 10 | import './styles/index.css' 11 | import './styles/index.scss' 12 | 13 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) 14 | root.render( 15 | 16 | 17 | , 18 | ) 19 | 20 | // If you want to start measuring performance in your app, pass a function 21 | // to log results (for example: reportWebVitals(console.log)) 22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 23 | reportWebVitals() 24 | 25 | // 禁止右键点击菜单在 release 中的展示 26 | function disableMenu() { 27 | if (window.location.hostname !== 'tauri.localhost') { 28 | return 29 | } 30 | document.addEventListener( 31 | 'contextmenu', 32 | (e) => { 33 | e.preventDefault() 34 | return false 35 | }, 36 | { capture: true }, 37 | ) 38 | } 39 | disableMenu() 40 | 41 | // ga4 42 | // ;(async () => { 43 | // const GAID = 'G-B365F44W6E' 44 | // try { 45 | // const conf = await store.readConfig() 46 | // const version = await runtime.getVersion() 47 | // ReactGA.initialize([ 48 | // { 49 | // trackingId: GAID, 50 | // gaOptions: { 51 | // app_version: version, 52 | // clientId: conf.uuid, 53 | // }, 54 | // gtagOptions: { 55 | // app_version: version, 56 | // clientId: conf.uuid, 57 | // }, 58 | // }, 59 | // ]) 60 | // } catch (e) { 61 | // ReactGA.initialize(GAID) 62 | // throw e 63 | // } 64 | // })() 65 | -------------------------------------------------------------------------------- /src/dialogs/ChatConfigDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Dialog, DialogContent, DialogActions, DialogTitle, DialogContentText, TextField } from '@mui/material' 3 | import { Session } from '../stores/types' 4 | import { useTranslation } from 'react-i18next' 5 | 6 | const { useEffect } = React 7 | 8 | interface Props { 9 | open: boolean 10 | session: Session 11 | save(session: Session): void 12 | close(): void 13 | } 14 | 15 | export default function ChatConfigDialog(props: Props) { 16 | const { t } = useTranslation() 17 | const [dataEdit, setDataEdit] = React.useState(props.session) 18 | 19 | useEffect(() => { 20 | setDataEdit(props.session) 21 | }, [props.session]) 22 | 23 | const onCancel = () => { 24 | props.close() 25 | setDataEdit(props.session) 26 | } 27 | 28 | const onSave = () => { 29 | if (dataEdit.name === '') { 30 | dataEdit.name = props.session.name 31 | } 32 | dataEdit.name = dataEdit.name.trim() 33 | props.save(dataEdit) 34 | props.close() 35 | } 36 | 37 | return ( 38 | 39 | {t('rename')} 40 | 41 | 42 | setDataEdit({ ...dataEdit, name: e.target.value })} 51 | /> 52 | 53 | 54 | 55 | 56 | 57 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/packages/runtime.ts: -------------------------------------------------------------------------------- 1 | import * as api from '@tauri-apps/api' 2 | import { Store } from 'tauri-plugin-store-api' 3 | 4 | const store = new Store('config.json') 5 | 6 | setInterval( 7 | async () => { 8 | try { 9 | await store.save() 10 | } catch (e) { 11 | console.log(e) 12 | } 13 | }, 14 | 5 * 60 * 1000, 15 | ) 16 | 17 | export const writeStore = async (key: string, value: any) => { 18 | await store.set(key, value) 19 | if (key === 'settings') { 20 | await store.save() 21 | } 22 | } 23 | 24 | export const readStore = async (key: string): Promise => { 25 | await handleCompatibilityV0_1() 26 | const value = await store.get(key) 27 | return value || undefined 28 | } 29 | 30 | async function handleCompatibilityV0_1() { 31 | // 第一次启动时,将旧版本的配置文件迁移到新的配置文件中 32 | try { 33 | const handled = await store.get('hasHandleCompatibilityV0_1') 34 | if (!handled) { 35 | const oldConfigJson = await api.fs.readTextFile('chatbox/config.json', { dir: api.fs.Dir.LocalData }) 36 | const oldConfig = JSON.parse(oldConfigJson) 37 | for (const key in oldConfig) { 38 | await store.set(key, oldConfig[key]) 39 | } 40 | await store.set('hasHandleCompatibilityV0_1', true) 41 | await store.save() 42 | } 43 | } catch (e) { 44 | console.log(e) 45 | } 46 | } 47 | 48 | export const shouldUseDarkColors = async (): Promise => { 49 | const theme = await api.window.appWindow.theme() 50 | return theme === 'dark' 51 | } 52 | 53 | export async function onSystemThemeChange(callback: () => void) { 54 | return api.window.appWindow.onThemeChanged(callback) 55 | } 56 | 57 | export const getVersion = async () => { 58 | return api.app.getVersion() 59 | } 60 | 61 | export const openLink = async (url: string) => { 62 | return api.shell.open(url) 63 | } 64 | 65 | export const getPlatform = async () => { 66 | return api.os.platform() 67 | } 68 | -------------------------------------------------------------------------------- /src/stores/types.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | import { ThemeMode } from '../theme' 3 | 4 | export type Message = OpenAIMessage & { 5 | id: string 6 | cancel?: () => void 7 | generating?: boolean 8 | model?: string 9 | } 10 | 11 | export interface Session { 12 | id: string 13 | name: string 14 | messages: Message[] 15 | starred?: boolean 16 | } 17 | 18 | export function createMessage(role: OpenAIRoleEnumType = OpenAIRoleEnum.User, content = ''): Message { 19 | return { 20 | id: uuidv4(), 21 | content: content, 22 | role: role, 23 | } 24 | } 25 | 26 | export function createSession(name = 'Untitled'): Session { 27 | return { 28 | id: uuidv4(), 29 | name: name, 30 | messages: [ 31 | { 32 | id: uuidv4(), 33 | role: 'system', 34 | content: 35 | 'You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.', 36 | }, 37 | ], 38 | } 39 | } 40 | 41 | export interface Settings { 42 | openaiKey: string 43 | apiHost: string 44 | model: string 45 | maxContextSize: string 46 | temperature: number 47 | maxTokens: string 48 | showWordCount?: boolean 49 | showTokenCount?: boolean 50 | showModelName?: boolean 51 | theme: ThemeMode 52 | language: string 53 | fontSize: number 54 | } 55 | 56 | export const OpenAIRoleEnum = { 57 | System: 'system', 58 | User: 'user', 59 | Assistant: 'assistant', 60 | } as const 61 | 62 | export type OpenAIRoleEnumType = (typeof OpenAIRoleEnum)[keyof typeof OpenAIRoleEnum] 63 | 64 | export interface OpenAIMessage { 65 | role: OpenAIRoleEnumType 66 | content: string 67 | name?: string 68 | } 69 | 70 | export interface Config { 71 | uuid: string 72 | } 73 | 74 | export interface SponsorAd { 75 | text: string 76 | url: string 77 | } 78 | 79 | export interface SponsorAboutBanner { 80 | type: 'picture' | 'picture-text' 81 | name: string 82 | pictureUrl: string 83 | link: string 84 | title: string 85 | description: string 86 | } 87 | -------------------------------------------------------------------------------- /src/packages/utils.ts: -------------------------------------------------------------------------------- 1 | import { Tiktoken } from 'js-tiktoken/lite' 2 | // @ts-ignore 3 | import cl100k_base from 'js-tiktoken/ranks/cl100k_base' 4 | 5 | import { Message } from '../stores/types' 6 | import copyToClipboardFallback from 'copy-to-clipboard' 7 | 8 | export function copyToClipboard(text: string) { 9 | navigator?.clipboard?.writeText(text) 10 | copyToClipboardFallback(text) 11 | } 12 | 13 | const pattern = 14 | /[a-zA-Z0-9_\u0392-\u03c9\u00c0-\u00ff\u0600-\u06ff\u0400-\u04ff]+|[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g 15 | export function countWord(data: string): number { 16 | try { 17 | data = typeof data === 'string' ? data : JSON.stringify(data) 18 | let m = data.match(pattern) 19 | let count = 0 20 | if (!m) { 21 | return 0 22 | } 23 | for (let i = 0; i < m.length; i++) { 24 | if (m[i].charCodeAt(0) >= 0x4e00) { 25 | count += m[i].length 26 | } else { 27 | count += 1 28 | } 29 | } 30 | return count 31 | } catch (e) { 32 | return -1 33 | } 34 | } 35 | 36 | const encoding = new Tiktoken(cl100k_base) 37 | function estimateTokens(str: string): number { 38 | str = typeof str === 'string' ? str : JSON.stringify(str) 39 | const tokens = encoding.encode(str) 40 | return tokens.length 41 | } 42 | 43 | // OpenAI Cookbook: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb 44 | export function estimateTokensFromMessages(messages: Message[]) { 45 | try { 46 | const tokensPerMessage = 3 47 | const tokensPerName = 1 48 | let ret = 0 49 | for (const msg of messages) { 50 | ret += tokensPerMessage 51 | ret += estimateTokens(msg.content) 52 | ret += estimateTokens(msg.role) 53 | if (msg.name) { 54 | ret += estimateTokens(msg.name) 55 | ret += tokensPerName 56 | } 57 | } 58 | ret += 3 // every reply is primed with <|start|>assistant<|message|> 59 | return ret 60 | } catch (e) { 61 | return -1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | # Webpack 86 | .webpack/ 87 | 88 | # Electron-Forge 89 | out/ 90 | 91 | publish.sh 92 | build.sh 93 | 94 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 95 | 96 | # dependencies 97 | /node_modules 98 | /.pnp 99 | .pnp.js 100 | 101 | # testing 102 | /coverage 103 | 104 | # next.js 105 | /.next/ 106 | /out/ 107 | 108 | # production 109 | /build 110 | 111 | # misc 112 | .DS_Store 113 | *.pem 114 | 115 | # debug 116 | npm-debug.log* 117 | yarn-debug.log* 118 | yarn-error.log* 119 | .pnpm-debug.log* 120 | 121 | # local env files 122 | .env*.local 123 | 124 | # IDE / Editor 125 | .idea 126 | 127 | # vercel 128 | .vercel 129 | 130 | # typescript 131 | *.tsbuildinfo 132 | next-env.d.ts 133 | 134 | tmp 135 | -------------------------------------------------------------------------------- /src/i18n/locales/zh-Hant/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "設定", 3 | "theme": "主題", 4 | "openai api key": "OpenAI API 金鑰", 5 | "show word count": "顯示字數", 6 | "show estimated token count": "顯示預估 Token 數", 7 | "proxy": "代理", 8 | "proxy warning": "您的 API 金鑰與所有訊息將會傳送至 {{apiHost}} 請確認您信任此網址。否則,可能會有 API 金鑰與資料外洩的風險。", 9 | "reset": "重設", 10 | "protocol warning": "所有資料皆透過 HTTP 協定傳輸的話,可能會造成 API 金鑰與資料外洩的風險。除非您非常確定且了解可能涉及的潛在風險,否則請考慮使用 HTTPS 協定。", 11 | "protocol error": "代理必須使用 HTTPHTTPS 協定。", 12 | "api host": "API 域名", 13 | "model": "模型", 14 | "token": "Token", 15 | "max tokens in context": "上下文中的 Token 上限", 16 | "max tokens per reply": "每則回覆的 Token 上限", 17 | "show model name": "顯示模型名稱", 18 | "settings modify warning": "這些是給專業開發者的設定。請勿在不了解這些設定的涵義下擅自更改,否則可能會產生請求錯誤。在修改前,請確認您的帳號可以存取被選擇的模型(不管是甚麼類型的帳號,其中一些模型都需要額外申請,否則會導致請求出現 404 錯誤);同時,請確認 Token 沒有超過被選擇模型的上限,否則上下文超出上限後會直接報錯。", 19 | "please make sure you know what you are doing.": "請確保您知道自己在做什麼。", 20 | "click here to": "而按下", 21 | "to default values.": "可以恢復預設值。", 22 | "cancel": "取消", 23 | "save": "儲存", 24 | "chat": "聊天", 25 | "new chat": "新聊天", 26 | "send": "傳送", 27 | "[Enter] send, [Shift+Enter] line break, [Ctrl+Enter] send without generating": "[Enter] 傳送、[Shift+Enter] 換行、[Ctrl+Enter] 傳送但不產生", 28 | "version": "版本", 29 | "rename": "改名", 30 | "name": "名稱", 31 | "clean": "清除", 32 | "delete confirmation": "此動作將永久刪除 {{sessionName}} 的內容。您確定要繼續嗎?", 33 | "clean it up": "清除", 34 | "copy": "複製", 35 | "edit": "編輯", 36 | "quote": "引用", 37 | "delete": "刪除", 38 | "api request failed:": "API 請求失敗:", 39 | "user": "使用者", 40 | "system": "系統", 41 | "assistant": "助理", 42 | "Prompt": "提示", 43 | "copied to clipboard": "已複製到剪貼簿", 44 | "language": "語言", 45 | "stop generating": "停止產生", 46 | "regenerate": "重新產生", 47 | "font size changed, effective after next launch": "字體大小已改變,將在下次啟動時生效", 48 | "star": "星標", 49 | "unstar": "取消星標", 50 | "About": "關於", 51 | "About Chatbox": "版本資訊", 52 | "close": "關閉", 53 | "About Message": "Chatbox 是一個免費開源的 GPT 桌面客戶端與調試工具,是你桌面上的最佳 AI 拍檔。由 <0>{{Author}} 與開源社區共同打造。", 54 | "Auther Message": "「剛開始我只是想開發一個方便自己使用的小工具,沒想到會有那麼多人喜歡它!如果你願意支持我的開發工作,可以適當進行捐贈,非常感謝。」", 55 | "Donate": "捐贈", 56 | "Or become a sponsor": "或成為贊助商", 57 | "Check Update": "檢查更新", 58 | "Homepage": "首頁", 59 | "Feedback": "建議回饋", 60 | "Roadmap": "未來規劃", 61 | "temperature": "嚴謹與想像(Temperature)", 62 | "meticulous": "嚴謹細緻", 63 | "creative": "想像發散", 64 | "Special thanks to the following sponsors:": "特別感謝以下贊助商:" 65 | } 66 | -------------------------------------------------------------------------------- /src/i18n/locales/zh-Hans/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "设置", 3 | "theme": "主题", 4 | "openai api key": "OpenAI API 密钥", 5 | "show word count": "显示字数统计", 6 | "show estimated token count": "显示预估 Token 字数统计", 7 | "proxy": "代理", 8 | "proxy warning": "您的 API 密钥和所有的消息将发送到 {{apiHost}} 请确认您信任此地址。否则,存在 API 密钥和数据泄露的风险。", 9 | "reset": "重置", 10 | "protocol warning": "所有数据传输都将通过 HTTP 协议进行,这可能会导致 API 密钥和数据泄露的风险。除非您完全确定并了解可能涉及的潜在风险,请考虑使用 HTTPS 协议。", 11 | "protocol error": "代理必须使用 HTTPHTTPS 协议。", 12 | "api host": "API 域名", 13 | "model": "模型", 14 | "token": "Token", 15 | "max tokens in context": "上下文中的最大 Token 数", 16 | "max tokens per reply": "每个回复的最大 Token 数", 17 | "show model name": "显示模型名称", 18 | "settings modify warning": "这些设置面向专业的开发者,如果您不知道这些设置的含义,请不要修改它们,否则可能会导致请求错误。在修改前,请确认您的账户可以访问被选择的模型(不管您是什么账户类型,其中一些模型都需要额外申请,否则会导致请求出现 404 错误);同时,请确认 token 数量没有超过被选择模型的限制,否则上下文超出限制后将会直接报错。", 19 | "please make sure you know what you are doing.": "请确保您知道自己在做什么。", 20 | "click here to": "点击这里", 21 | "to default values.": "恢复默认值。", 22 | "cancel": "取消", 23 | "save": "保存", 24 | "chat": "对话", 25 | "new chat": "新对话", 26 | "send": "发送", 27 | "[Enter] send, [Shift+Enter] line break, [Ctrl+Enter] send without generating": "[回车键] 发送,[Shift+回车键] 换行, [Ctrl+回车键] 发送但不生成", 28 | "version": "版本", 29 | "rename": "重命名", 30 | "name": "名称", 31 | "clean": "清空", 32 | "delete confirmation": "此操作将永久删除 {{sessionName}} 的内容。您确定要继续吗?", 33 | "clean it up": "清空", 34 | "copy": "复制", 35 | "edit": "编辑", 36 | "quote": "引用", 37 | "delete": "删除", 38 | "api request failed:": "API 请求失败:", 39 | "user": "用户", 40 | "system": "系统", 41 | "assistant": "助手", 42 | "Prompt": "Prompt", 43 | "copied to clipboard": "已复制到剪贴板", 44 | "language": "语言", 45 | "stop generating": "停止生成", 46 | "regenerate": "重新生成", 47 | "font size changed, effective after next launch": "字体大小已改变,将在下次启动时生效", 48 | "star": "星标", 49 | "unstar": "取消星标", 50 | "About": "关于", 51 | "About Chatbox": "版本信息", 52 | "close": "关闭", 53 | "About Message": "Chatbox 是一个免费开源的 GPT 桌面客户端与调试工具,是你桌面上的最佳 AI 拍档。由 <0>{{Author}} 与开源社区共同打造。", 54 | "Auther Message": "“刚开始我只是想开发一个方便自己使用的小工具,没想到会有那么多人喜欢它!如果你愿意支持我的开发工作,可以适当进行捐赠,非常感谢。”", 55 | "Donate": "捐赠", 56 | "Or become a sponsor": "或成为赞助商", 57 | "Check Update": "检查更新", 58 | "Homepage": "首页", 59 | "Feedback": "建议反馈", 60 | "Roadmap": "未来规划", 61 | "temperature": "严谨与想象(Temperature)", 62 | "meticulous": "严谨细致", 63 | "creative": "想象发散", 64 | "Special thanks to the following sponsors:": "特别鸣谢以下品牌的赞助:" 65 | } 66 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | If you still haven't found the answer you're looking for, feel free to submit an [Issue](https://github.com/Bin-Huang/chatbox/issues/new/choose) as well. 4 | 5 | ### 1001 6 | 7 | #### Message sending failed, showing `Failed to fetch`? 8 | 9 | This issue occurs when Chatbox cannot connect to the AI model server you've set up. Please check your current network environment and make sure it can connect properly to the AI model server. 10 | 11 | For OpenAI API users, if you've chosen OpenAI API as the AI model provider (meaning you've selected `OpenAI API` in the AI Provider settings), it's typically because Chatbox cannot access the `API HOST` you've set. By default, Chatbox uses `https://api.openai.com` as the API HOST. Please make sure your current network can access this service. 12 | 13 | ### 1002 14 | 15 | #### Everything was working fine before, but now I keep getting an error: `{"error":{"message":"You exceeded your current quota, please check your plan and billing details`? 16 | 17 | If everything was working fine before and now you're unable to use the service, with each message sending attempt resulting in the following error: 18 | 19 | ``` 20 | {"error":{"message":"You exceeded your current quota, please check your plan and billing details.","type":"insufficient_quota","param":null,"code":null}} 21 | ``` 22 | 23 | Please note that this issue is not related to Chatbox. In this situation, it's likely that you're using your own OpenAI API account and your free quota has either been used up or expired (usually due to expiration). You need to log in to your OpenAI account's dashboard and link a credit card to continue using the service. The OpenAI API account has many requirements for credit cards. If your card doesn't meet these requirements, you'll need to resolve this issue yourself (it can be quite frustrating). 24 | 25 | **Consider using `Chatbox AI`:** If you don't want to deal with these issues, you can also use Chatbox's built-in `Chatbox AI` service. This service allows you to enjoy AI capabilities without any hassle. Go to the settings page and set the AI Provider to `Chatbox AI`, and you'll see the corresponding options. 26 | 27 | ### 1003 28 | 29 | #### Unable to use GPT-4? 30 | 31 | If you select GPT-4 and receive a similar error message when sending messages: 32 | 33 | ``` 34 | {"error":{"message":"The model: gpt-4-32k does not exist","type":"invalid_request_error","param":null,"code":"model_not_found"}} 35 | ``` 36 | 37 | This issue often occurs when you're using your own OpenAI account and have selected the GPT-4 model, but your OpenAI API account does not support GPT-4. As of July 4, 2023, all OpenAI API accounts require a request to be submitted to OpenAI before the GPT-4 model can be used. Here's the application link: https://openai.com/waitlist/gpt-4-api. 38 | -------------------------------------------------------------------------------- /team-sharing/README.md: -------------------------------------------------------------------------------- 1 | # Chatbox Team Sharing 2 | 3 | Chatbox allows your team members to share the resources of the same OpenAI API account without exposing your API KEY. 4 | 5 | The following tutorial will help you quickly set up a shared server. It may involve server login, command-line input, etc. If you are not familiar with these operations, you can ask your technical colleague for help or inquire with AI Assistanct. Trust me, it's not difficult. 6 | 7 | ## 1. Prepare a Server 8 | 9 | You can launch a cloud server on platforms such as AWS, Google Cloud, Digital Ocean, Vultr, Oracle Cloud, etc. Please note that the server's network must be able to access openai.com. 10 | 11 | ## 2. Environment Installation 12 | 13 | Log into your server and execute the following command: 14 | 15 | ```shell 16 | curl -fsSL https://get.docker.com -o get-docker.sh 17 | sh get-docker.sh 18 | ``` 19 | 20 | ## 3. Start the Chatbox Shared Server (HTTP) 21 | 22 | - Replace `` with your OpenAI API KEY. 23 | - Run the following command to start the server. 24 | 25 | ```shell 26 | docker run -p 80:80 -p 443:443 \ 27 | -v ./caddy_config:/config -v ./caddy_data:/data \ 28 | -e KEY= \ 29 | bensdocker/chatbox-team 30 | ``` 31 | 32 | Example: 33 | 34 | ``` 35 | docker run -p 80:80 -p 443:443 \ 36 | -v ./caddy_config:/config -v ./caddy_data:/data \ 37 | -e KEY=sk-xxxxxxxxxxxxxxxxxxx \ 38 | bensdocker/chatbox-team 39 | ``` 40 | 41 | ## 4. Start the Chatbox Shared Server (HTTPS, recommended) 42 | 43 | If you have a domain name, you can use HTTPS to start the server, so that all conversation messages are encrypted as ciphertext during network transmission, which is more secure in terms of privacy. 44 | 45 | - Map the domain to this server (and wait for five minutes for it to take effect); 46 | - Replace `` with your domain name; 47 | - Replace `` with your OpenAI API KEY; 48 | - Execute the following command to start the server. 49 | 50 | ```shell 51 | docker run -p 80:80 -p 443:443 \ 52 | -v ./caddy_config:/config -v ./caddy_data:/data \ 53 | -e HOST= \ 54 | -e KEY= \ 55 | bensdocker/chatbox-team 56 | ``` 57 | 58 | Example: 59 | 60 | ``` 61 | docker run -p 80:80 -p 443:443 \ 62 | -v ./caddy_config:/config -v ./caddy_data:/data \ 63 | -e HOST=proxy.chatbox.run \ 64 | -e KEY=sk-xxxxxxxxxxxxxxxxxx \ 65 | bensdocker/chatbox-team 66 | ``` 67 | 68 | ## 5. Share the Server Address 69 | 70 | - If you run with HTTP, the address is `http://:80`; 71 | - If you run with HTTPS, the address is `https://`; 72 | 73 | Share the server address with your team members. They only need to fill in this address in the API Host field in Chatbox settings, without filling in the API KEY, to share the OpenAI API resources. 74 | 75 | ![](./demo_http.png) 76 | 77 | ![](./demo_https.png) 78 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:meta'; 2 | 3 | html > body { 4 | /* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, 5 | Arial, sans-serif; */ 6 | /* padding: 0; */ 7 | margin: auto; 8 | } 9 | 10 | html[data-theme='dark'] { 11 | // ------------ highlight.js --------------- 12 | 13 | // https://stackoverflow.com/questions/59257368/how-to-dynamically-change-the-theme-using-highlight-js 14 | @include meta.load-css('highlight.js/styles/github-dark'); 15 | 16 | // ------------ 滚动条 --------------- 17 | 18 | /* 设置滚动条的宽度, 背景色与边框 */ 19 | ::-webkit-scrollbar { 20 | width: 6px; 21 | height: 6px; 22 | background-color: #333333; 23 | border-radius: 6px; 24 | } 25 | 26 | ::scrollbar { 27 | width: 6px; 28 | height: 6px; 29 | background-color: #333333; 30 | border-radius: 6px; 31 | } 32 | 33 | /* 滚动条上的滑块 */ 34 | ::-webkit-scrollbar-thumb { 35 | background-color: #b1b1b1; 36 | border-radius: 6px; 37 | } 38 | 39 | /* 鼠标悬停在滑块上时 */ 40 | ::-webkit-scrollbar-thumb:hover { 41 | background-color: #8c8c8c; 42 | } 43 | 44 | /* 滚动条的轨道 */ 45 | ::-webkit-scrollbar-track { 46 | background-color: #333333; 47 | border-radius: 6px; 48 | } 49 | 50 | /* 鼠标悬停在滚动条轨道上时 */ 51 | ::-webkit-scrollbar-track:hover { 52 | background-color: #2c2c2c; 53 | } 54 | 55 | /* 滚动条上下按钮 */ 56 | ::-webkit-scrollbar-button { 57 | display: none; 58 | } 59 | 60 | a { 61 | color: #fff; 62 | } 63 | } 64 | 65 | html[data-theme='light'] { 66 | // ------------ highlight.js --------------- 67 | 68 | // https://stackoverflow.com/questions/59257368/how-to-dynamically-change-the-theme-using-highlight-js 69 | // @include meta.load-css('highlight.js/styles/github'); 70 | @include meta.load-css('highlight.js/styles/github-dark'); 71 | 72 | // ------------ 滚动条 --------------- 73 | 74 | /* 设置滚动条的宽度, 背景色与边框 */ 75 | ::-webkit-scrollbar { 76 | width: 6px; 77 | height: 6px; 78 | background-color: #f5f5f5; 79 | border-radius: 6px; 80 | } 81 | 82 | ::scrollbar { 83 | width: 6px; 84 | height: 6px; 85 | background-color: #f5f5f5; 86 | border-radius: 6px; 87 | } 88 | 89 | /* 滚动条上的滑块 */ 90 | ::-webkit-scrollbar-thumb { 91 | background-color: #c6c6c6; 92 | border-radius: 6px; 93 | } 94 | 95 | /* 鼠标悬停在滑块上时 */ 96 | ::-webkit-scrollbar-thumb:hover { 97 | background-color: #a0a0a0; 98 | } 99 | 100 | /* 滚动条的轨道 */ 101 | ::-webkit-scrollbar-track { 102 | background-color: #f5f5f5; 103 | border-radius: 6px; 104 | } 105 | 106 | /* 鼠标悬停在滚动条轨道上时 */ 107 | ::-webkit-scrollbar-track:hover { 108 | background-color: #dcdcdc; 109 | } 110 | 111 | /* 滚动条上下按钮 */ 112 | ::-webkit-scrollbar-button { 113 | display: none; 114 | } 115 | 116 | a { 117 | color: #333; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/i18n/locales/jp/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "設定", 3 | "theme": "テーマ", 4 | "openai api key": "OpenAI API キー", 5 | "show word count": "単語数を表示", 6 | "show estimated token count": "おおよそのトークン数を表示", 7 | "proxy": "プロキシ", 8 | "proxy warning": "メッセージ内容またAPIキーは該当アドレスに送信されます: {{apiHost}}. 該当アドレスを信用しているかは再度ご確認ください。APIキーまたデータ漏えいの危険性ある場合がございます。", 9 | "reset": "リセット", 10 | "protocol warning": "すべてのデータ転送はHTTPのプロトコルによる行われ, APIキーまたデータ漏えいの危険性ある場合がございます。潜在的なリスクを完全に理解していない限り, HTTPSを代わりに利用してください。", 11 | "protocol error": "プロキシは HTTPまたHTTPSを使用になければなりません。", 12 | "api host": "API ホスト", 13 | "model": "モデル", 14 | "token": "トークン", 15 | "max tokens in context": "コンテキスト内の最大トークン数", 16 | "max tokens per reply": "返信ごとの最大トークン数", 17 | "show model name": "モデル名を表示", 18 | "settings modify warning": "以下の設定の操作は、有識者を対象としています。リクエストエラーを起こす場合がございますので、分かりかねない場合、変更しないで下さい。", 19 | "before making any modifications, please verify that your account has access to the selected models (some models require additional joining of the waiting list, regardless of your account type, otherwise, it will result in 404 errors).": "変更を行う前に、お持ちのアカウントが選択したモデルにアクセス件があることを確認してください(一部のモデルは、アカウントの種類に関係なく、ウェイトリストに登録する必要あるので、404エラーを起こす場合がございます)。", 20 | "please make sure you know what you are doing.": "操作に大変ご注意下さい。", 21 | "click here to": "こちらをクリックすると", 22 | "to default values.": "されます。", 23 | "cancel": "キャンセル", 24 | "save": "保存", 25 | "chat": "チャット", 26 | "new chat": "新規チャット", 27 | "send": "送信", 28 | "[Enter] send, [Shift+Enter] line break, [Ctrl+Enter] send without generating": "[Enter] 送信, [Shift+Enter] 改行, [Ctrl+Enter] 生成せずに送信", 29 | "version": "バージョン", 30 | "rename": "題名変更", 31 | "name": "題名", 32 | "clean": "一掃する", 33 | "delete confirmation": "該当するトークはこちらの操作によって削除されます:{{sessionName}}. よろしいですか?", 34 | "clean it up": "一掃する", 35 | "copy": "コピー", 36 | "edit": "編集", 37 | "quote": "引用", 38 | "delete": "削除", 39 | "api request failed:": "API リクエストが失敗しました:", 40 | "user": "ユーザー", 41 | "system": "システム", 42 | "assistant": "アシスタント", 43 | "Prompt": "プロンプト", 44 | "copied to clipboard": "クリップボードに保存されました", 45 | "language": "言語", 46 | "stop generating": "生成を停止する", 47 | "regenerate": "再生成する", 48 | "font size changed, effective after next launch": "フォントサイズ変更は、再起動時に反映されます。", 49 | "star": "お気に入り", 50 | "unstar": "お気に入り削除", 51 | "About": "詳細", 52 | "About Chatbox": "Chatboxに関して", 53 | "close": "閉じる", 54 | "About Message": "最先端のデスクトップ用コパイロット。Chatboxは無料でオープンソース、GPTのためのアプリケーションかつ開発ツールです。<0>{{Author}} とコミュニティより作られました。", 55 | "Auther Message": "Chatboxは個人用に作りましたが、多くの方々に喜んでいただけて、とても嬉しく思います!もし開発をサポートしていただけるのであれば、寄付を大変ありがたく受け取りますが、もちろん任意です。ご支援いただける場合、心から感謝いたします。Bennより", 56 | "Donate": "寄付する", 57 | "Or become a sponsor": "スポンサーになる", 58 | "Check Update": "リリースノート", 59 | "Homepage": "ホームページ", 60 | "Feedback": "フィードバック", 61 | "Roadmap": "ロードマップ", 62 | "temperature": "温度差", 63 | "meticulous": "細かい", 64 | "creative": "クリエイティブ", 65 | "Special thanks to the following sponsors:": "以下のスポンサーに大変感謝しています:" 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/Block.scss: -------------------------------------------------------------------------------- 1 | .msg-block { 2 | .msg-content p { 3 | margin: 0.6rem 0 0.4rem 0; 4 | } 5 | .msg-content ol { 6 | padding-inline-start: 25px; 7 | } 8 | .msg-content ul { 9 | padding-inline-start: 25px; 10 | } 11 | .msg-content img { 12 | width: 100%; 13 | max-width: 40rem; 14 | } 15 | } 16 | 17 | // markdown table 18 | html[data-theme='light'] .msg-block { 19 | table { 20 | background-color: #f8f8f8; 21 | margin: 0 auto; 22 | border-collapse: collapse; 23 | font-size: 1em; 24 | font-family: Arial, sans-serif; 25 | line-height: 1.2; 26 | border: 1px solid #ddd; 27 | } 28 | table th, 29 | table td { 30 | padding: 0.5em 1.2em; 31 | border: 1px solid #ddd; 32 | text-align: center; 33 | } 34 | table th { 35 | background-color: #e5e5e5; 36 | font-weight: bold; 37 | color: #333; 38 | } 39 | table tr:nth-child(even) { 40 | background-color: #f2f2f2; 41 | } 42 | } 43 | html[data-theme='dark'] .msg-block { 44 | table { 45 | background-color: #333; 46 | color: #fff; 47 | margin: 0 auto; 48 | border-collapse: collapse; 49 | font-size: 1em; 50 | font-family: Arial, sans-serif; 51 | line-height: 1.2; 52 | border: 1px solid #666; 53 | } 54 | table th, 55 | table td { 56 | padding: 0.5em 1.2em; 57 | border: 1px solid #666; 58 | text-align: center; 59 | } 60 | table th { 61 | background-color: #555; 62 | font-weight: bold; 63 | } 64 | table tr:nth-child(even) { 65 | background-color: #444; 66 | } 67 | } 68 | 69 | // code block 70 | .code-block-wrapper { 71 | width: 100%; 72 | } 73 | .code-header { 74 | margin: 0; 75 | background-color: #333; 76 | color: white; 77 | font-size: 12px; 78 | display: flex; 79 | justify-content: space-between; 80 | } 81 | .code-block { 82 | box-sizing: border-box; 83 | padding: 10px; 84 | max-width: 100%; 85 | overflow: auto; 86 | margin: 0; 87 | } 88 | .rendering .code-block-wrapper div.copy-action { 89 | display: none; 90 | } 91 | .code-block-wrapper .code-lang { 92 | margin-left: 6px; 93 | padding: 2px; 94 | } 95 | .code-block-wrapper div.copy-action { 96 | margin-right: 6px; 97 | cursor: pointer; 98 | display: inline-block; 99 | } 100 | .code-block-wrapper div.copy-action { 101 | background-color: rgba(144, 202, 249, 0.16); 102 | padding: 2px; 103 | } 104 | .code-block-wrapper div.copy-action:hover { 105 | background-color: rgba(144, 202, 249, 0.36); 106 | } 107 | .code-block-wrapper:hover div.copy-action { 108 | opacity: 1; 109 | } 110 | 111 | // message 112 | html[data-theme='light'] { 113 | .assistant-msg { 114 | background-color: #fafafa; 115 | } 116 | // .system-msg { 117 | // background-color: #fafafa; 118 | // } 119 | } 120 | html[data-theme='dark'] { 121 | .assistant-msg { 122 | background-color: #212121; 123 | } 124 | // .system-msg { 125 | // background-color: #212121; 126 | // } 127 | } 128 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: 'publish' 2 | on: 3 | push: 4 | tags: [v\d+\.\d+\.\d+] 5 | 6 | jobs: 7 | publish-tauri: 8 | permissions: 9 | contents: write 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | include: 14 | - target: aarch64-apple-darwin 15 | platform: macos-latest 16 | - target: x86_64-apple-darwin 17 | platform: macos-latest 18 | - target: x86_64-unknown-linux-gnu 19 | platform: ubuntu-20.04 20 | - target: x86_64-pc-windows-msvc 21 | platform: windows-latest 22 | 23 | runs-on: ${{ matrix.platform }} 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - name: Get version 28 | id: get_version 29 | uses: battila7/get-version-action@v2 30 | 31 | - name: setup node 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: 16 35 | 36 | - name: install Rust stable 37 | uses: dtolnay/rust-toolchain@stable 38 | 39 | - name: Rust cache 40 | uses: swatinem/rust-cache@v2 41 | with: 42 | workspaces: './src-tauri -> target' 43 | 44 | - name: install dependencies (ubuntu only) 45 | if: matrix.platform == 'ubuntu-20.04' 46 | run: | 47 | sudo apt-get update 48 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libx11-dev libxdo-dev libxcb-shape0-dev libxcb-xfixes0-dev 49 | - name: install dependencies (mac only) 50 | if: matrix.platform == 'macos-latest' 51 | run: | 52 | rustup target add aarch64-apple-darwin 53 | - uses: actions/cache@v2 54 | with: 55 | path: '**/node_modules' 56 | key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock') }} 57 | 58 | - name: install frontend dependencies 59 | run: yarn install # change this to npm or pnpm depending on which one you use 60 | 61 | - name: Change Version 62 | env: 63 | VERSION: '${{ steps.get_version.outputs.version-without-v }}' 64 | run: make change-version 65 | 66 | - name: Build Tauri App 67 | uses: tauri-apps/tauri-action@dev 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }} 70 | ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }} 71 | APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} 72 | APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} 73 | APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} 74 | APPLE_ID: ${{ secrets.APPLE_ID }} 75 | APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} 76 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 77 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 78 | with: 79 | tagName: Chatbox-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version 80 | releaseName: 'Chatbox v__VERSION__' 81 | releaseBody: 'See the assets to download this version and install.' 82 | releaseDraft: true 83 | prerelease: false 84 | args: --target ${{matrix.target}} 85 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json", 3 | "build": { 4 | "beforeBuildCommand": "npm run build:react", 5 | "beforeDevCommand": "cross-env BROWSER=none npm run start:react", 6 | "devPath": "http://localhost:3000", 7 | "distDir": "../build", 8 | "withGlobalTauri": true 9 | }, 10 | "package": { 11 | "productName": "chatbox", 12 | "version": "0.0.1" 13 | }, 14 | "tauri": { 15 | "allowlist": { 16 | "shell": { 17 | "open": true 18 | }, 19 | "app": { 20 | "all": true 21 | }, 22 | "window": { 23 | "all": true 24 | }, 25 | "fs": { 26 | "all": true, 27 | "scope": [ 28 | "$APPDATA", 29 | "$APPDATA/*", 30 | "$APPCONFIG", 31 | "$APPCONFIG/*", 32 | "$LOCALDATA/chatbox/*" 33 | ] 34 | }, 35 | "http": { 36 | "all": true 37 | }, 38 | "dialog": { 39 | "all": true, 40 | "open": true, 41 | "save": true 42 | }, 43 | "os": { 44 | "all": true 45 | } 46 | }, 47 | "bundle": { 48 | "active": true, 49 | "category": "DeveloperTool", 50 | "copyright": "", 51 | "deb": { 52 | "depends": [] 53 | }, 54 | "externalBin": [], 55 | "icon": [ 56 | "icons/32x32.png", 57 | "icons/128x128.png", 58 | "icons/128x128@2x.png", 59 | "icons/icon.icns", 60 | "icons/icon.ico" 61 | ], 62 | "identifier": "xyz.chatboxapp.app", 63 | "longDescription": "", 64 | "macOS": { 65 | "entitlements": null, 66 | "exceptionDomain": "", 67 | "frameworks": [], 68 | "providerShortName": null, 69 | "signingIdentity": null 70 | }, 71 | "resources": [], 72 | "shortDescription": "", 73 | "targets": "all", 74 | "windows": { 75 | "certificateThumbprint": null, 76 | "digestAlgorithm": "sha256", 77 | "timestampUrl": "", 78 | "webviewInstallMode": { 79 | "silent": true, 80 | "type": "embedBootstrapper" 81 | } 82 | } 83 | }, 84 | "security": { 85 | "csp": null 86 | }, 87 | "updater": { 88 | "active": true, 89 | "endpoints": [ 90 | "https://releases.chatboxai.app/chatbox/{{target}}/{{current_version}}", 91 | "https://gist.githubusercontent.com/Bin-Huang/cf0b7684e28b30d760ba02417bbac916/raw/chatbox.releases.json", 92 | "https://gist.githubusercontent.com/Bin-Huang/cf0b7684e28b30d760ba02417bbac916/raw" 93 | ], 94 | "dialog": true, 95 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDRCNkM4MDE2NDJBNTU3ODcKUldTSFY2VkNGb0JzUzZyT2Fzamx5dDB4QVQ5cHU1aU1tR3RYRVh4V204REkvMjFheWNzRG80dDEK" 96 | }, 97 | "windows": [ 98 | { 99 | "fullscreen": false, 100 | "height": 800, 101 | "resizable": true, 102 | "title": "chatbox", 103 | "width": 1000 104 | } 105 | ] 106 | } 107 | } -------------------------------------------------------------------------------- /src/i18n/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "Settings", 3 | "theme": "Theme", 4 | "openai api key": "OpenAI API Key", 5 | "show word count": "Show word count", 6 | "show estimated token count": "Show estimated token count", 7 | "proxy": "Proxy", 8 | "proxy warning": "Your API Key and all messages will be sent to {{apiHost}}. Please confirm that you trust this address. Otherwise, there is a risk of API Key and data leakage.", 9 | "reset": "Reset", 10 | "protocol warning": "All data transfers are being conducted through the HTTP protocol, which may lead to the risk of API Key and data leakage.Unless you are completely certain and understand the potential risks involved, please consider using the HTTPS protocol instead.", 11 | "protocol error": "Proxy must use HTTP or HTTPS.", 12 | "api host": "API Host", 13 | "model": "Model", 14 | "token": "Token", 15 | "max tokens in context": "Max Tokens in Context", 16 | "max tokens per reply": "Max Tokens per Reply", 17 | "show model name": "Show Model Name", 18 | "settings modify warning": "These settings are aimed at professional developers. If you do not understand the meaning of these settings, please do not modify them, as it may result in request errors. Before making any modifications, please verify that your account has access to the selected models (some models require additional joining of the waiting list, regardless of your account type, otherwise, it will result in 404 errors). Please make sure that the number of tokens does not exceed the limit for the selected model, otherwise, an error message will occur once the context exceeds the limit.", 19 | "please make sure you know what you are doing.": "Please make sure you know what you are doing.", 20 | "click here to": "Click here to", 21 | "to default values.": "to default values.", 22 | "cancel": "Cancel", 23 | "save": "Save", 24 | "chat": "Chat", 25 | "new chat": "New Chat", 26 | "send": "Send", 27 | "[Enter] send, [Shift+Enter] line break, [Ctrl+Enter] send without generating": "[Enter] send, [Shift+Enter] line break, [Ctrl+Enter] send without generating", 28 | "version": "Version", 29 | "rename": "Rename", 30 | "name": "Name", 31 | "clean": "Clean", 32 | "delete confirmation": "This action will permanently delete all non-system messages in {{sessionName}}. Are you sure you want to continue?", 33 | "clean it up": "Clean it up", 34 | "copy": "Copy", 35 | "edit": "Edit", 36 | "quote": "Quote", 37 | "delete": "Delete", 38 | "api request failed:": "API Request Failed:", 39 | "user": "User", 40 | "system": "System", 41 | "assistant": "Assistant", 42 | "Prompt": "Prompt", 43 | "copied to clipboard": "Copied to clipboard", 44 | "language": "Language", 45 | "stop generating": "Stop generating", 46 | "regenerate": "Regenerate", 47 | "font size changed, effective after next launch": "Font size changed, effective after next launch", 48 | "star": "Star", 49 | "unstar": "Unstar", 50 | "About": "About", 51 | "About Chatbox": "About Chatbox", 52 | "close": "Close", 53 | "About Message": "Your Ultimate Copilot on the Desktop. Chatbox is a free and open-source desktop application and devtools for GPT. Made by <0>{{Author}} and the community.", 54 | "Auther Message": "I made Chatbox for my own use and it's great to see so many people enjoying it! If you'd like to support development, a donation would be greatly appreciated, though it is entirely optional. Many thanks, Benn", 55 | "Donate": "Donate", 56 | "Or become a sponsor": "Or become a sponsor", 57 | "Check Update": "Check Update", 58 | "Homepage": "Homepage", 59 | "Feedback": "Feedback", 60 | "Roadmap": "Roadmap", 61 | "temperature": "Temperature", 62 | "meticulous": "Meticulous", 63 | "creative": "Creative", 64 | "Special thanks to the following sponsors:": "Special thanks to the following sponsors:" 65 | } 66 | -------------------------------------------------------------------------------- /src/components/InputBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, MutableRefObject } from 'react' 2 | import { Stack, Grid, Button, Typography, TextField } from '@mui/material' 3 | import { Message, createMessage } from '../stores/types' 4 | import { useTranslation } from 'react-i18next' 5 | import SendIcon from '@mui/icons-material/Send' 6 | 7 | export default function InputBox(props: { 8 | onSubmit: (newMsg: Message, needGenerating?: boolean) => void 9 | quoteCache: string 10 | setQuotaCache(cache: string): void 11 | textareaRef: MutableRefObject 12 | }) { 13 | const { t } = useTranslation() 14 | const [messageInput, setMessageInput] = useState('') 15 | useEffect(() => { 16 | if (props.quoteCache !== '') { 17 | setMessageInput(props.quoteCache) 18 | props.setQuotaCache('') 19 | props.textareaRef?.current?.focus() 20 | } 21 | }, [props.quoteCache]) 22 | const submit = (needGenerating = true) => { 23 | if (messageInput.trim() === '') { 24 | return 25 | } 26 | props.onSubmit(createMessage('user', messageInput), needGenerating) 27 | setMessageInput('') 28 | } 29 | useEffect(() => { 30 | function keyboardShortcut(e: KeyboardEvent) { 31 | if (e.key === 'i' && (e.metaKey || e.ctrlKey)) { 32 | props.textareaRef?.current?.focus() 33 | } 34 | } 35 | window.addEventListener('keydown', keyboardShortcut) 36 | return () => { 37 | window.removeEventListener('keydown', keyboardShortcut) 38 | } 39 | }, []) 40 | 41 | return ( 42 |
{ 44 | e.preventDefault() 45 | submit() 46 | }} 47 | > 48 | 49 | 50 | 51 | setMessageInput(event.target.value)} 57 | fullWidth 58 | maxRows={12} 59 | autoFocus 60 | id="message-input" 61 | onKeyDown={(event) => { 62 | if ( 63 | event.keyCode === 13 && 64 | !event.shiftKey && 65 | !event.ctrlKey && 66 | !event.altKey && 67 | !event.metaKey 68 | ) { 69 | event.preventDefault() 70 | submit() 71 | return 72 | } 73 | if (event.keyCode === 13 && event.ctrlKey) { 74 | event.preventDefault() 75 | submit(false) 76 | return 77 | } 78 | }} 79 | /> 80 | 81 | 82 | 85 | 86 | 87 | 88 | {t('[Enter] send, [Shift+Enter] line break, [Ctrl+Enter] send without generating')} 89 | 90 | 91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatbox", 3 | "productName": "chatbox", 4 | "version": "0.0.1", 5 | "private": true, 6 | "description": "Chatbox is a desktop app for multiple cutting-edge LLM models that available on Windows, Mac, Linux", 7 | "scripts": { 8 | "start:react": "react-scripts start", 9 | "build:react": "react-scripts build", 10 | "test:react": "react-scripts test", 11 | "eject:react": "react-scripts eject", 12 | "dev": "tauri dev", 13 | "build": "tauri build", 14 | "tauri": "tauri", 15 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 16 | "check": "npx tsc --noEmit", 17 | "prettier": "npx prettier -w ." 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/Bin-Huang/chatbox.git" 22 | }, 23 | "keywords": [], 24 | "author": { 25 | "name": "bennhuang", 26 | "email": "tohuangbin@gmail.com" 27 | }, 28 | "license": "GPL-3.0 license", 29 | "devDependencies": { 30 | "@tauri-apps/cli": "^1.2.3", 31 | "@types/highlight.js": "^10.1.0", 32 | "@types/markdown-it": "^12.2.3", 33 | "@types/markdown-it-link-attributes": "^3.0.1", 34 | "@types/react-dom": "^18.0.11", 35 | "@types/uuid": "^9.0.1", 36 | "@typescript-eslint/eslint-plugin": "^5.62.0", 37 | "@typescript-eslint/parser": "^5.62.0", 38 | "cross-env": "^7.0.3", 39 | "css-loader": "^6.0.0", 40 | "eslint": "^8.46.0", 41 | "eslint-plugin-import": "^2.25.0", 42 | "fork-ts-checker-webpack-plugin": "^7.2.13", 43 | "node-loader": "^2.0.0", 44 | "prettier": "^3.0.1", 45 | "react-scripts": "^3.0.1", 46 | "sass": "^1.59.3", 47 | "sass-loader": "^13.2.0", 48 | "style-loader": "^3.0.0", 49 | "ts-loader": "^9.2.2", 50 | "ts-node": "^10.9.1" 51 | }, 52 | "dependencies": { 53 | "@dnd-kit/core": "^6.0.8", 54 | "@dnd-kit/modifiers": "^6.0.1", 55 | "@dnd-kit/sortable": "^7.0.2", 56 | "@dnd-kit/utilities": "^3.2.1", 57 | "@emotion/react": "^11.10.6", 58 | "@emotion/styled": "^11.10.6", 59 | "@mui/icons-material": "^5.11.11", 60 | "@mui/material": "^5.11.11", 61 | "@tauri-apps/api": "^1.2.0", 62 | "@testing-library/jest-dom": "^5.16.5", 63 | "@testing-library/react": "^13.4.0", 64 | "@testing-library/user-event": "^13.5.0", 65 | "@traptitech/markdown-it-katex": "^3.6.0", 66 | "axios": "^1.3.4", 67 | "copy-to-clipboard": "^3.3.3", 68 | "eventsource-parser": "^1.0.0", 69 | "form-data": "^4.0.0", 70 | "fs-extra": "^11.1.1", 71 | "github-markdown-css": "^5.2.0", 72 | "highlight.js": "^11.7.0", 73 | "i18next": "^22.4.13", 74 | "js-tiktoken": "^1.0.12", 75 | "markdown-it": "^13.0.1", 76 | "markdown-it-link-attributes": "^4.0.1", 77 | "material-ui-popup-state": "^5.0.4", 78 | "react": "^18.2.0", 79 | "react-dom": "^18.2.0", 80 | "react-ga4": "^2.1.0", 81 | "react-i18next": "^12.2.0", 82 | "tauri-plugin-store-api": "https://github.com/tauri-apps/tauri-plugin-store", 83 | "typescript": "^4.9.5", 84 | "uuid": "^9.0.0", 85 | "web-vitals": "^2.1.4" 86 | }, 87 | "browserslist": { 88 | "production": [ 89 | ">0.2%", 90 | "not dead", 91 | "not op_mini all" 92 | ], 93 | "development": [ 94 | "last 1 chrome version", 95 | "last 1 firefox version", 96 | "last 1 safari version" 97 | ] 98 | }, 99 | "prettier": { 100 | "tabWidth": 4, 101 | "singleQuote": true, 102 | "printWidth": 120, 103 | "semi": false, 104 | "overrides": [ 105 | { 106 | "files": [ 107 | ".prettierrc", 108 | ".eslintrc" 109 | ], 110 | "options": { 111 | "parser": "json" 112 | } 113 | } 114 | ] 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/theme/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme, ThemeProvider } from '@mui/material/styles' 2 | import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useState } from 'react' 3 | import useEvent from '../hooks/useEvent' 4 | import useStore from '../stores/store' 5 | import { ThemeMode, fetchThemeDesign, RealThemeMode } from './index' 6 | import * as runtime from '../packages/runtime' 7 | import CssBaseline from '@mui/material/CssBaseline' 8 | 9 | export interface ThemeSwitcherAction { 10 | setMode: (mode: ThemeMode) => void 11 | } 12 | 13 | const ThemeSwitchContext = createContext<[ThemeMode, ThemeSwitcherAction] | null>(null) 14 | 15 | interface ThemeSwitcherProviderProps { 16 | children: React.ReactNode 17 | } 18 | 19 | const REAL_THEME_MODE = 'REAL_THEME_MODE;' 20 | const THEME_MODE = 'THEME_MODE' 21 | 22 | function getThemeModeFromLocal(key: string, defaultValue: T) { 23 | const localMode = localStorage.getItem(key) 24 | if (localMode) { 25 | return Number(localMode) 26 | } else { 27 | return defaultValue 28 | } 29 | } 30 | 31 | export function ThemeSwitcherProvider(props: ThemeSwitcherProviderProps) { 32 | const { settings } = useStore() 33 | const [mode, setMode] = useState(getThemeModeFromLocal(THEME_MODE, ThemeMode.System)) 34 | // `shouldUseDarkColors` becomes asynchronous after being called by tauri, 35 | // here need to use a useState to convert `shouldUseDarkColors` to synchronous 36 | const [realMode, setRealMode] = useState(getThemeModeFromLocal(REAL_THEME_MODE, ThemeMode.Dark)) 37 | 38 | // "shouldUseDarkColors" is asynchronous, after calling "changeRealMode", "mode" may have changed (eg: System -> Dark), 39 | // then "shouldUseDarkColors" should not be used to determine "realMode" 40 | const changeRealMode = useEvent((scopeMode: ThemeMode, newMode: RealThemeMode) => { 41 | if (scopeMode !== mode) return 42 | setRealMode(newMode) 43 | localStorage.setItem(REAL_THEME_MODE, newMode.toString()) 44 | }) 45 | 46 | function changeMode(newMode: ThemeMode) { 47 | setMode(newMode) 48 | localStorage.setItem(THEME_MODE, newMode.toString()) 49 | if (newMode !== ThemeMode.System) { 50 | changeRealMode(mode, newMode) 51 | } 52 | } 53 | 54 | const themeSwitcherContext = useMemo<[ThemeMode, ThemeSwitcherAction]>( 55 | () => [ 56 | mode, 57 | { 58 | setMode: changeMode, 59 | }, 60 | ], 61 | [mode], 62 | ) 63 | 64 | const theme = useMemo(() => createTheme(fetchThemeDesign(realMode, settings.fontSize)), [realMode, settings]) 65 | 66 | useLayoutEffect(() => { 67 | if (mode !== ThemeMode.System) return 68 | // watch system theme change 69 | const handleModeChange = async () => { 70 | const isDark = await runtime.shouldUseDarkColors() 71 | changeRealMode(mode, isDark ? ThemeMode.Dark : ThemeMode.Light) 72 | } 73 | 74 | handleModeChange() 75 | const disposePromise = runtime.onSystemThemeChange(handleModeChange) 76 | return () => { 77 | disposePromise.then((dispose) => dispose()) 78 | } 79 | }, [mode]) 80 | 81 | useEffect(() => { 82 | if (settings.theme !== mode) { 83 | changeMode(settings.theme) 84 | } 85 | }, [settings.theme]) 86 | 87 | useLayoutEffect(() => { 88 | document.querySelector('html')?.setAttribute('data-theme', realMode === ThemeMode.Dark ? 'dark' : 'light') 89 | }, [realMode]) 90 | 91 | return useMemo( 92 | () => ( 93 | 94 | 95 | 96 | {props.children} 97 | 98 | 99 | ), 100 | [themeSwitcherContext, theme, props.children], 101 | ) 102 | } 103 | 104 | export function useThemeSwicher() { 105 | return useContext(ThemeSwitchContext) as any 106 | } 107 | 108 | export default ThemeSwitchContext 109 | -------------------------------------------------------------------------------- /src/i18n/locales/fr/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "Paramètres", 3 | "theme": "Thème", 4 | "openai api key": "Clé API OpenAI", 5 | "show word count": "Afficher le nombre de mots", 6 | "show estimated token count": "Afficher le nombre de jetons estimé", 7 | "proxy": "Proxy", 8 | "proxy warning": "Votre clé API et tous les messages seront envoyés à {{apiHost}}. Veuillez confirmer que vous faites confiance à cette adresse. Sinon, il y a un risque de fuite de clé API et de données.", 9 | "reset": "Réinitialiser", 10 | "protocol warning": "Tous les transferts de données sont effectués via le protocole HTTP, ce qui peut entraîner un risque de fuite de clé API et de données. Sauf si vous êtes complètement certain et comprenez les risques potentiels impliqués, veuillez envisager d'utiliser le protocole HTTPS à la place.", 11 | "protocol error": "Le proxy doit utiliser HTTP ou HTTPS.", 12 | "api host": "Hôte API", 13 | "model": "Modèle", 14 | "token": "Jeton", 15 | "max tokens in context": "Nombre maximal de jetons dans le contexte", 16 | "max tokens per reply": "Nombre maximal de jetons par réponse", 17 | "show model name": "Afficher le nom du modèle", 18 | "settings modify warning": "Ces paramètres sont destinés aux développeurs professionnels. Si vous ne comprenez pas la signification de ces paramètres, veuillez ne pas les modifier, car cela peut entraîner des erreurs de demande. Avant de faire des modifications, veuillez vérifier que votre compte a accès aux modèles sélectionnés (certains modèles nécessitent une inscription supplémentaire sur la liste d'attente, indépendamment de votre type de compte, sinon cela entraînera des erreurs 404). Veuillez vous assurer que le nombre de jetons ne dépasse pas la limite pour le modèle sélectionné, sinon un message d'erreur apparaîtra une fois que le contexte aura dépassé la limite.", 19 | "please make sure you know what you are doing.": "Veuillez vous assurer que vous savez ce que vous faites.", 20 | "click here to": "Cliquez ici pour", 21 | "to default values.": "aux valeurs par défaut.", 22 | "cancel": "Annuler", 23 | "save": "Enregistrer", 24 | "chat": "Chat", 25 | "new chat": "Nouveau chat", 26 | "send": "Envoyer", 27 | "[Enter] send, [Shift+Enter] line break, [Ctrl+Enter] send without generating": "[Entrée] envoyer, [Maj+Entrée] saut de ligne, [Ctrl+Entrée] envoyer sans générer", 28 | "version": "Version", 29 | "rename": "Renommer", 30 | "name": "Nom", 31 | "clean": "Nettoyer", 32 | "delete confirmation": "Cette action supprimera définitivement tous les messages non système dans {{sessionName}}. Êtes-vous sûr de vouloir continuer ?", 33 | "clean it up": "Nettoyer", 34 | "copy": "Copier", 35 | "edit": "Modifier", 36 | "quote": "Citer", 37 | "delete": "Supprimer", 38 | "api request failed:": "La demande API a échoué :", 39 | "user": "Utilisateur", 40 | "system": "Système", 41 | "assistant": "Assistant", 42 | "Prompt": "Invite", 43 | "copied to clipboard": "Copié dans le presse-papiers", 44 | "language": "Langue", 45 | "stop generating": "Arrêter la génération", 46 | "regenerate": "Régénérer", 47 | "font size changed, effective after next launch": "La taille de la police a été modifiée, elle sera effective après le prochain lancement", 48 | "star": "Étoile", 49 | "unstar": "Retirer l'étoile", 50 | "About": "À propos", 51 | "About Chatbox": "À propos de Chatbox", 52 | "close": "Fermer", 53 | "About Message": "Votre copilote ultime sur le bureau. Chatbox est une application de bureau et des outils de développement gratuits et open-source pour GPT. Dévelopée par <0>{{Author}} et la communauté", 54 | "Auther Message": "J'ai créé Chatbox pour mon usage personnel et c'est génial de voir autant de personnes l'apprécier ! Si vous souhaitez soutenir le développement, un don serait grandement apprécié, bien que cela soit entièrement facultatif. Merci beaucoup, Benn", 55 | "Donate": "Faire un don", 56 | "Or become a sponsor": "Ou devenir un sponsor", 57 | "Check Update": "Vérifier les mises à jour", 58 | "Homepage": "Page d'accueil", 59 | "Feedback": "Retour d'information", 60 | "Roadmap": "Feuille de route", 61 | "temperature": "Température", 62 | "meticulous": "Méticuleux", 63 | "creative": "Créatif", 64 | "Special thanks to the following sponsors:": "Un grand merci aux sponsors suivants :" 65 | } 66 | -------------------------------------------------------------------------------- /src/packages/llm.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../stores/types' 2 | import * as utils from './utils' 3 | import { createParser } from 'eventsource-parser' 4 | 5 | export interface OnTextCallbackResult { 6 | // response content 7 | text: string 8 | // cancel for fetch 9 | cancel: () => void 10 | } 11 | 12 | export async function chat( 13 | apiKey: string, 14 | host: string, 15 | maxContextSize: string, 16 | maxTokens: string, 17 | modelName: string, 18 | temperature: number, 19 | msgs: Message[], 20 | onText?: (option: OnTextCallbackResult) => void, 21 | onError?: (error: Error) => void, 22 | ) { 23 | if (msgs.length === 0) { 24 | throw new Error('No messages to replay') 25 | } 26 | const head = msgs[0].role === 'system' ? msgs[0] : undefined 27 | if (head) { 28 | msgs = msgs.slice(1) 29 | } 30 | 31 | const maxTokensNumber = Number(maxTokens) 32 | const maxLen = Number(maxContextSize) 33 | let totalLen = head ? utils.estimateTokensFromMessages([head]) : 0 34 | 35 | let prompts: Message[] = [] 36 | for (let i = msgs.length - 1; i >= 0; i--) { 37 | const msg = msgs[i] 38 | const msgTokenSize: number = utils.estimateTokensFromMessages([msg]) 39 | if (msgTokenSize + totalLen > maxLen) { 40 | break 41 | } 42 | prompts = [msg, ...prompts] 43 | totalLen += msgTokenSize 44 | } 45 | if (head) { 46 | prompts = [head, ...prompts] 47 | } 48 | 49 | // fetch has been canceled 50 | let hasCancel = false 51 | // abort signal for fetch 52 | const controller = new AbortController() 53 | const cancel = () => { 54 | hasCancel = true 55 | controller.abort() 56 | } 57 | 58 | let fullText = '' 59 | try { 60 | const messages = prompts.map((msg) => ({ role: msg.role, content: msg.content })) 61 | const response = await fetch(`${host}/v1/chat/completions`, { 62 | method: 'POST', 63 | headers: { 64 | Authorization: `Bearer ${apiKey}`, 65 | 'Content-Type': 'application/json', 66 | }, 67 | body: JSON.stringify({ 68 | messages, 69 | model: modelName, 70 | max_tokens: maxTokensNumber, 71 | temperature, 72 | stream: true, 73 | }), 74 | signal: controller.signal, 75 | }) 76 | await handleSSE(response, (message) => { 77 | if (message === '[DONE]') { 78 | return 79 | } 80 | const data = JSON.parse(message) 81 | if (data.error) { 82 | throw new Error(`Error from OpenAI: ${JSON.stringify(data)}`) 83 | } 84 | const text = data.choices[0]?.delta?.content 85 | if (text !== undefined) { 86 | fullText += text 87 | if (onText) { 88 | onText({ text: fullText, cancel }) 89 | } 90 | } 91 | }) 92 | } catch (error) { 93 | // if a cancellation is performed 94 | // do not throw an exception 95 | // otherwise the content will be overwritten. 96 | if (hasCancel) { 97 | return 98 | } 99 | if (onError) { 100 | onError(error as any) 101 | } 102 | throw error 103 | } 104 | return fullText 105 | } 106 | 107 | export async function handleSSE(response: Response, onMessage: (message: string) => void) { 108 | if (!response.ok) { 109 | const error = await response.json().catch(() => null) 110 | throw new Error(error ? JSON.stringify(error) : `${response.status} ${response.statusText}`) 111 | } 112 | if (response.status !== 200) { 113 | throw new Error(`Error from OpenAI: ${response.status} ${response.statusText}`) 114 | } 115 | if (!response.body) { 116 | throw new Error('No response body') 117 | } 118 | const parser = createParser((event) => { 119 | if (event.type === 'event') { 120 | onMessage(event.data) 121 | } 122 | }) 123 | for await (const chunk of iterableStreamAsync(response.body)) { 124 | const str = new TextDecoder().decode(chunk) 125 | parser.feed(str) 126 | } 127 | } 128 | 129 | export async function* iterableStreamAsync(stream: ReadableStream): AsyncIterableIterator { 130 | const reader = stream.getReader() 131 | try { 132 | while (true) { 133 | const { value, done } = await reader.read() 134 | if (done) { 135 | return 136 | } else { 137 | yield value 138 | } 139 | } 140 | } finally { 141 | reader.releaseLock() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/components/SessionItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { ListItemText, MenuItem, Divider, IconButton, Typography, ListItemIcon } from '@mui/material' 3 | import { Session } from '../stores/types' 4 | import FileCopyIcon from '@mui/icons-material/FileCopy' 5 | import EditIcon from '@mui/icons-material/Edit' 6 | import ChatBubbleOutlineOutlinedIcon from '@mui/icons-material/ChatBubbleOutlineOutlined' 7 | import MoreHorizOutlinedIcon from '@mui/icons-material/MoreHorizOutlined' 8 | import DeleteForeverIcon from '@mui/icons-material/DeleteForever' 9 | import StyledMenu from './StyledMenu' 10 | import { useTranslation } from 'react-i18next' 11 | import StarIcon from '@mui/icons-material/Star' 12 | import StarOutlineIcon from '@mui/icons-material/StarOutline' 13 | 14 | export interface Props { 15 | session: Session 16 | selected: boolean 17 | switchMe: () => void 18 | deleteMe: () => void 19 | copyMe: () => void 20 | switchStarred: () => void 21 | editMe: () => void 22 | } 23 | 24 | export default function SessionItem(props: Props) { 25 | const { t } = useTranslation() 26 | const { session, selected, switchMe, deleteMe, copyMe, switchStarred, editMe } = props 27 | const [hovering, setHovering] = useState(false) 28 | const [anchorEl, setAnchorEl] = React.useState(null) 29 | const open = Boolean(anchorEl) 30 | const handleClick = (event: React.MouseEvent) => { 31 | event.stopPropagation() 32 | event.preventDefault() 33 | setAnchorEl(event.currentTarget) 34 | } 35 | const handleClose = () => { 36 | setAnchorEl(null) 37 | } 38 | 39 | return ( 40 | <> 41 | switchMe()} 45 | onMouseEnter={() => { 46 | setHovering(true) 47 | }} 48 | onMouseOver={() => { 49 | setHovering(true) 50 | }} 51 | onMouseLeave={() => { 52 | setHovering(false) 53 | }} 54 | > 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {session.name} 63 | 64 | 65 | { 66 | 67 | {session.starred ? ( 68 | 69 | ) : ( 70 | hovering && 71 | )} 72 | 73 | } 74 | 75 | 83 | { 86 | editMe() 87 | handleClose() 88 | }} 89 | disableRipple 90 | > 91 | 92 | {t('rename')} 93 | 94 | 95 | { 98 | copyMe() 99 | handleClose() 100 | }} 101 | disableRipple 102 | > 103 | 104 | {t('copy')} 105 | 106 | { 109 | switchStarred() 110 | handleClose() 111 | }} 112 | disableRipple 113 | > 114 | {session.starred ? ( 115 | <> 116 | 117 | {t('unstar')} 118 | 119 | ) : ( 120 | <> 121 | 122 | {t('star')} 123 | 124 | )} 125 | 126 | 127 | 128 | 129 | { 132 | setAnchorEl(null) 133 | handleClose() 134 | deleteMe() 135 | }} 136 | disableRipple 137 | > 138 | 139 | {t('delete')} 140 | 141 | 142 | 143 | ) 144 | } 145 | -------------------------------------------------------------------------------- /script/release-update.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const fs = require('fs-extra') 3 | 4 | // node script/release-update.js v0.4.2 cloudflare 5 | 6 | const data = { 7 | version: 'v0.1.15', 8 | notes: 'New version', 9 | pub_date: '2023-03-19T14:50:47.517Z', 10 | platforms: { 11 | 'darwin-x86_64': { 12 | signature: '', 13 | url: 'https://github.com/Bin-Huang/chatbox/releases/download/Chatbox-v0.1.15/chatbox.app.tar.gz', 14 | }, 15 | 'darwin-aarch64': { 16 | signature: '', 17 | url: 'https://github.com/Bin-Huang/chatbox/releases/download/Chatbox-v0.1.15/chatbox.app.tar.gz', 18 | }, 19 | 'linux-x86_64': { 20 | signature: '', 21 | url: 'https://github.com/Bin-Huang/chatbox/releases/download/Chatbox-v0.1.15/chatbox_0.1.15_amd64.AppImage.tar.gz', 22 | }, 23 | 'windows-x86_64': { 24 | signature: '', 25 | url: 'https://github.com/Bin-Huang/chatbox/releases/download/Chatbox-v0.1.15/chatbox_0.1.15_x64_en-US.msi.zip', 26 | }, 27 | win64: { 28 | signature: '', 29 | url: 'https://github.com/Bin-Huang/chatbox/releases/download/Chatbox-v0.1.15/chatbox_0.1.15_x64_en-US.msi.zip', 30 | }, 31 | linux: { 32 | signature: '', 33 | url: 'https://github.com/Bin-Huang/chatbox/releases/download/Chatbox-v0.1.15/chatbox_0.1.15_amd64.AppImage.tar.gz', 34 | }, 35 | darwin: { 36 | signature: '', 37 | url: 'https://github.com/Bin-Huang/chatbox/releases/download/Chatbox-v0.1.15/chatbox.app.tar.gz', 38 | }, 39 | }, 40 | } 41 | 42 | main() 43 | async function main() { 44 | let version = process.argv[2] 45 | let storageFlag = process.argv[3] || 'github' 46 | 47 | const res = await axios.get('https://api.github.com/repos/Bin-Huang/chatbox/releases') 48 | const release = version ? res.data.find((r) => r.tag_name.endsWith(version)) : res.data[0] 49 | 50 | data.version = release.tag_name.replace('Chatbox-', '') 51 | data.pub_date = new Date().toISOString() 52 | 53 | version = data.version 54 | console.log(version) 55 | 56 | const dir = './tmp/' + version 57 | fs.ensureDirSync(dir) 58 | 59 | const promises = [] 60 | for (const asset of release.assets) { 61 | promises.push(handleAsset(asset, version, dir, storageFlag)) 62 | } 63 | await Promise.all(promises) 64 | 65 | fs.writeFileSync(`${dir}/update.json`, JSON.stringify(data, null, 4)) 66 | console.log(data) 67 | console.log(dir) 68 | } 69 | 70 | async function handleAsset(asset, version, dir, storageFlag) { 71 | let link = asset.browser_download_url 72 | if (storageFlag === 'cos') { 73 | link = `https://chatbox-1252521402.cos.ap-hongkong.myqcloud.com/${version}/${asset.name}` 74 | } 75 | if (storageFlag === 'cloudflare') { 76 | link = `https://pub-0f2a372de68244aabdee60c9d82c4c6c.r2.dev/${version}/${asset.name}` 77 | } 78 | if (asset.name.endsWith('.app.tar.gz') && asset.name.includes('x64')) { 79 | if (storageFlag !== 'github') { 80 | await download(asset.browser_download_url, `${dir}/${asset.name}`) 81 | } 82 | data.platforms['darwin'].url = link 83 | data.platforms['darwin-x86_64'].url = link 84 | return 85 | } 86 | if (asset.name.endsWith('.app.tar.gz') && asset.name.includes('aarch64')) { 87 | if (storageFlag !== 'github') { 88 | await download(asset.browser_download_url, `${dir}/${asset.name}`) 89 | } 90 | data.platforms['darwin-aarch64'].url = link 91 | return 92 | } 93 | if (asset.name.endsWith('.AppImage.tar.gz')) { 94 | if (storageFlag !== 'github') { 95 | await download(asset.browser_download_url, `${dir}/${asset.name}`) 96 | } 97 | data.platforms['linux'].url = link 98 | data.platforms['linux-x86_64'].url = link 99 | return 100 | } 101 | if (asset.name.endsWith('.msi.zip')) { 102 | if (storageFlag !== 'github') { 103 | await download(asset.browser_download_url, `${dir}/${asset.name}`) 104 | } 105 | data.platforms['win64'].url = link 106 | data.platforms['windows-x86_64'].url = link 107 | return 108 | } 109 | 110 | if (asset.name.endsWith('.app.tar.gz.sig') && asset.name.includes('x64')) { 111 | const res = await axios.get(asset.browser_download_url) 112 | data.platforms['darwin'].signature = res.data 113 | data.platforms['darwin-x86_64'].signature = res.data 114 | return 115 | } 116 | if (asset.name.endsWith('.app.tar.gz.sig') && asset.name.includes('aarch64')) { 117 | const res = await axios.get(asset.browser_download_url) 118 | data.platforms['darwin-aarch64'].signature = res.data 119 | return 120 | } 121 | if (asset.name.endsWith('.AppImage.tar.gz.sig')) { 122 | const res = await axios.get(asset.browser_download_url) 123 | data.platforms['linux'].signature = res.data 124 | data.platforms['linux-x86_64'].signature = res.data 125 | return 126 | } 127 | if (asset.name.endsWith('.msi.zip.sig')) { 128 | const res = await axios.get(asset.browser_download_url) 129 | data.platforms['win64'].signature = res.data 130 | data.platforms['windows-x86_64'].signature = res.data 131 | return 132 | } 133 | } 134 | 135 | // download file from url to filepath 136 | async function download(url, filepath) { 137 | const res = await axios.get(url, { responseType: 'stream' }) 138 | const writer = fs.createWriteStream(filepath) 139 | res.data.pipe(writer) 140 | await new Promise((resolve, reject) => { 141 | writer.on('finish', resolve) 142 | writer.on('error', reject) 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /src/stores/store.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react' 2 | import { Settings, createSession, Session, Message, Config } from './types' 3 | import * as defaults from '../packages/defaults' 4 | import { v4 as uuidv4 } from 'uuid' 5 | import { ThemeMode } from '../theme' 6 | import * as runtime from '../packages/runtime' 7 | import * as remote from '../packages/remote' 8 | import { useTranslation } from 'react-i18next' 9 | 10 | // setting store 11 | 12 | export function getDefaultSettings(): Settings { 13 | return { 14 | openaiKey: '', 15 | apiHost: 'https://api.openai.com', 16 | model: 'gpt-3.5-turbo', 17 | temperature: 0.7, 18 | maxContextSize: '4000', 19 | maxTokens: '2048', 20 | showWordCount: false, 21 | showTokenCount: false, 22 | showModelName: false, 23 | theme: ThemeMode.System, 24 | language: 'en', 25 | fontSize: 13, 26 | } 27 | } 28 | 29 | export async function readSettings(): Promise { 30 | const setting: Settings | undefined = await runtime.readStore('settings') 31 | if (!setting) { 32 | return getDefaultSettings() 33 | } 34 | // 兼容早期版本 35 | const settingWithDefaults = Object.assign({}, getDefaultSettings(), setting) 36 | 37 | return settingWithDefaults 38 | } 39 | 40 | export async function writeSettings(settings: Settings) { 41 | if (!settings.apiHost) { 42 | settings.apiHost = getDefaultSettings().apiHost 43 | } 44 | console.log('writeSettings.apiHost', settings.apiHost) 45 | return runtime.writeStore('settings', settings) 46 | } 47 | 48 | export async function readConfig(): Promise { 49 | let config: Config | undefined = await runtime.readStore('configs') 50 | if (!config) { 51 | config = { uuid: uuidv4() } 52 | await runtime.writeStore('configs', config) 53 | } 54 | return config 55 | } 56 | 57 | export async function writeConfig(config: Config) { 58 | return runtime.writeStore('configs', config) 59 | } 60 | 61 | // session store 62 | 63 | export async function readSessions(settings: Settings): Promise { 64 | const sessions: Session[] | undefined = await runtime.readStore('chat-sessions') 65 | if (!sessions) { 66 | return defaults.sessions 67 | } 68 | if (sessions.length === 0) { 69 | return [createSession()] 70 | } 71 | return sessions.map((s: any) => { 72 | // 兼容旧版本的数据 73 | if (!s.model) { 74 | s.model = getDefaultSettings().model 75 | } 76 | return s 77 | }) 78 | } 79 | 80 | export async function writeSessions(sessions: Session[]) { 81 | return runtime.writeStore('chat-sessions', sessions) 82 | } 83 | 84 | // react hook 85 | 86 | export default function useStore() { 87 | const { i18n } = useTranslation() 88 | 89 | const [version, _setVersion] = useState('unknown') 90 | const [needCheckUpdate, setNeedCheckUpdate] = useState(false) 91 | const updateCheckTimer = useRef() 92 | useEffect(() => { 93 | const handler = async () => { 94 | const version = await runtime.getVersion() 95 | _setVersion(version) 96 | try { 97 | const config = await readConfig() 98 | const os = await runtime.getPlatform() 99 | const needUpdate = await remote.checkNeedUpdate(version, os, config) 100 | setNeedCheckUpdate(needUpdate) 101 | } catch (e) { 102 | console.log(e) 103 | setNeedCheckUpdate(true) 104 | } 105 | } 106 | handler() 107 | updateCheckTimer.current = setInterval(handler, 10 * 60 * 1000) 108 | return () => { 109 | if (updateCheckTimer.current) { 110 | clearInterval(updateCheckTimer.current) 111 | updateCheckTimer.current = undefined 112 | } 113 | } 114 | }, []) 115 | 116 | const [settings, _setSettings] = useState(getDefaultSettings()) 117 | const [needSetting, setNeedSetting] = useState(false) 118 | useEffect(() => { 119 | readSettings().then((settings) => { 120 | _setSettings(settings) 121 | if (settings.openaiKey === '') { 122 | setNeedSetting(true) 123 | } 124 | i18n.changeLanguage(settings.language).then() 125 | }) 126 | }, []) 127 | const setSettings = (settings: Settings) => { 128 | _setSettings(settings) 129 | writeSettings(settings) 130 | i18n.changeLanguage(settings.language).then() 131 | } 132 | 133 | const [chatSessions, _setChatSessions] = useState([createSession()]) 134 | const [currentSession, switchCurrentSession] = useState(chatSessions[0]) 135 | useEffect(() => { 136 | readSessions(settings).then((sessions: Session[]) => { 137 | _setChatSessions(sessions) 138 | switchCurrentSession(sessions[0]) 139 | }) 140 | }, []) 141 | const setSessions = (sessions: Session[]) => { 142 | _setChatSessions(sessions) 143 | writeSessions(sessions) 144 | } 145 | 146 | const deleteChatSession = (target: Session) => { 147 | const sessions = chatSessions.filter((s) => s.id !== target.id) 148 | if (sessions.length === 0) { 149 | sessions.push(createSession()) 150 | } 151 | if (target.id === currentSession.id) { 152 | switchCurrentSession(sessions[0]) 153 | } 154 | setSessions(sessions) 155 | } 156 | const updateChatSession = (session: Session) => { 157 | const sessions = chatSessions.map((s) => { 158 | if (s.id === session.id) { 159 | return session 160 | } 161 | return s 162 | }) 163 | setSessions(sessions) 164 | if (session.id === currentSession.id) { 165 | switchCurrentSession(session) 166 | } 167 | } 168 | const createChatSession = (session: Session, ix?: number) => { 169 | const sessions = [...chatSessions, session] 170 | setSessions(sessions) 171 | switchCurrentSession(session) 172 | } 173 | const createEmptyChatSession = () => { 174 | createChatSession(createSession()) 175 | } 176 | 177 | const setMessages = (session: Session, messages: Message[]) => { 178 | updateChatSession({ 179 | ...session, 180 | messages, 181 | }) 182 | } 183 | 184 | const [toasts, _setToasts] = useState<{ id: string; content: string }[]>([]) 185 | const addToast = (content: string) => { 186 | const id = uuidv4() 187 | _setToasts([...toasts, { id, content }]) 188 | } 189 | const removeToast = (id: string) => { 190 | _setToasts(toasts.filter((t) => t.id !== id)) 191 | } 192 | 193 | return { 194 | version, 195 | needCheckUpdate, 196 | 197 | settings, 198 | setSettings, 199 | needSetting, 200 | 201 | chatSessions, 202 | createChatSession, 203 | updateChatSession, 204 | deleteChatSession, 205 | createEmptyChatSession, 206 | 207 | setSessions, 208 | currentSession, 209 | switchCurrentSession, 210 | 211 | toasts, 212 | addToast, 213 | removeToast, 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/dialogs/AboutDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { 3 | Button, 4 | Paper, 5 | Badge, 6 | Box, 7 | Divider, 8 | Dialog, 9 | DialogContent, 10 | DialogActions, 11 | DialogTitle, 12 | Stack, 13 | } from '@mui/material' 14 | import iconPNG from '../assets/icon.png' 15 | import { Trans, useTranslation } from 'react-i18next' 16 | import * as runtime from '../packages/runtime' 17 | import * as remote from '../packages/remote' 18 | import { SponsorAboutBanner } from '../stores/types' 19 | 20 | interface Props { 21 | open: boolean 22 | version: string 23 | lang: string 24 | close(): void 25 | } 26 | 27 | export default function AboutDialog(props: Props) { 28 | const { t } = useTranslation() 29 | const [sponsorBanners, setSponsorBanners] = useState([]) 30 | useEffect(() => { 31 | if (props.open) { 32 | remote.listSponsorAboutBanner().then(setSponsorBanners) 33 | } else { 34 | setSponsorBanners([]) 35 | } 36 | }, [props.open]) 37 | return ( 38 | 39 | {t('About Chatbox')} 40 | 41 | 42 | 43 |

Chatbox(v{props.version})

44 | 45 | , 53 | ]} 54 | /> 55 | 56 |
57 | 58 | 59 | 67 | 68 | 74 | 80 | 86 | 87 | 91 | {t('Auther Message')} 92 | 93 | 99 | 107 | 108 | 109 | 110 | {sponsorBanners.length > 0 && ( 111 | 112 | {t('Special thanks to the following sponsors:')} 113 | 114 | )} 115 | :not(style)': { 120 | m: 1, 121 | }, 122 | justifyContent: 'center', 123 | opacity: 0.8, 124 | }} 125 | > 126 | {sponsorBanners.map((item) => { 127 | return ( 128 | 142 | {item.type === 'picture' ? ( 143 | <> 144 | 145 | 149 | 150 | 151 | ) : ( 152 | <> 153 | 154 | 155 | 156 | 157 | {item.title} 158 | 159 | 160 | {item.description} 161 | 162 | 163 | )} 164 | 165 | ) 166 | })} 167 | 168 |
169 | 170 | 171 | 172 |
173 | ) 174 | } 175 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 |

2 | English | 3 | 简体中文 4 |

5 | 6 |

7 | 8 | Chatbox 9 |

10 |

11 | Chatbox 是一个 AI 模型桌面客户端,支持 ChatGPT、Claude、Google Gemini、Ollama 等主流模型,适用于 Windows、Mac、Linux、Web、Android 和 iOS 全平台 12 |

13 | 14 |

15 | 16 | macOS 17 | 18 | 19 | Windows 20 | 21 | 22 | Linux 23 | 24 | 25 | 下载量 26 | 27 | 28 | Twitter 29 | 30 |

31 | 32 | ## ⏬ 下载桌面版安装包 33 | 34 | 35 | 36 | 39 | 42 | 45 | 46 | 47 | 54 | 61 | 68 | 75 | 76 |
37 | Windows 38 | 40 | MacOS 41 | 43 | Linux 44 |
48 | 49 | 50 |
51 | Setup.exe 52 |
53 |
55 | 56 | 57 |
58 | Intel 59 |
60 |
62 | 63 | 64 |
65 | M1/M2 66 |
67 |
69 | 70 | 71 |
72 | AppImage 73 |
74 |
77 | 78 | 或者访问 **[GitHub Releases](https://github.com/Bin-Huang/chatbox/releases)** 下载最新版本或任何以前的版本。 79 | 80 | ## ⏬ 下载 iOS/Android App 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | .APK 91 | 92 | 93 | ## 🌟 特性 94 | 95 | Chatbox - Better UI & Desktop App for ChatGPT, Claude and other LLMs. | Product Hunt 96 | 97 | 应用截图 98 | 99 | 应用截图 100 | 101 | 应用截图 102 | 103 | # 特性 104 | 105 | - **本地数据存储** 106 | :floppy_disk: 您的数据保留在您的设备上,确保数据永不丢失并保护您的隐私。 107 | 108 | - **无需部署、直接安装的安装包** 109 | :package: 通过可下载的安装包快速开始使用。无需复杂设置! 110 | 111 | - **支持多个 LLM 提供商** 112 | :gear: 无缝集成多种 AI 模型: 113 | 114 | - OpenAI (ChatGPT) 115 | - Azure OpenAI 116 | - Claude 117 | - Google Gemini Pro 118 | - Ollama (启用对本地模型的访问,如 llama2、Mistral、Mixtral、codellama、vicuna、yi 和 solar) 119 | - ChatGLM-6B 120 | 121 | - **使用 Dall-E-3 生成图像** 122 | :art: 使用 Dall-E-3 创建您想象中的图像。 123 | 124 | - **增强提示** 125 | :speech_balloon: 高级提示功能,精炼并聚焦您的查询以获得更好的响应。 126 | 127 | - **键盘快捷键** 128 | :keyboard: 使用加速您工作流程的快捷键保持高效。 129 | 130 | - **Markdown、Latex 和代码高亮** 131 | :scroll: 使用 Markdown 和 Latex 的全部功能生成消息,并结合各种编程语言的语法高亮,提高可读性和呈现效果。 132 | 133 | - **提示库和消息引用** 134 | :books: 保存和组织提示以供重复使用,并引用消息以在讨论中提供上下文。 135 | 136 | - **流式回复** 137 | :arrow_forward: 通过即时、渐进式回复快速响应您的互动。 138 | 139 | - **人体工程学 UI 和深色主题** 140 | :new_moon: 用户友好的界面,带有夜间模式选项,减少长时间使用时的眼睛疲劳。 141 | 142 | - **团队协作** 143 | :busts_in_silhouette: 轻松协作并在团队中共享 OpenAI API 资源。[了解更多](./team-sharing/README.md) 144 | 145 | - **跨平台可用性** 146 | :computer: 聊天盒已为 Windows、Mac、Linux 用户准备就绪。 147 | 148 | - **通过 Web 版本随处访问** 149 | :globe_with_meridians: 在任何设备上使用带有浏览器的 Web 应用程序,随时随地。 150 | 151 | - **iOS 和 Android** 152 | :phone: 使用移动应用程序,随时随地在您的指尖上带来这种能力。 153 | 154 | - **多语言支持** 155 | :earth_americas: 通过提供多种语言的支持,迎合全球受众: 156 | 157 | - English 158 | - 简体中文 (Simplified Chinese) 159 | - 繁體中文 (Traditional Chinese) 160 | - 日本語 (Japanese) 161 | - 한국어 (Korean) 162 | - Français (French) 163 | - Deutsch (German) 164 | - Русский (Russian) 165 | 166 | - **更多...** 167 | :sparkles: 不断增强体验,加入新功能! 168 | 169 | ## 常见问题解答 170 | 171 | - [常见问题](./FAQ.md) 172 | 173 | ## 路线图 174 | 175 | - [x] AI 聊天和会话管理 176 | - [x] 本地存储所有重要的消息数据。 177 | - [x] Markdown 178 | - [x] 流式回复 179 | - [x] API 主机配置 180 | - [x] 自动生成标签标题 181 | - [x] 清除消息的按钮。 182 | - [x] 夜间/深色模式 183 | - [x] 消息令牌估算 184 | - [x] GPT4 185 | - [x] i18n 186 | - [x] 代码块的复制按钮 187 | - [x] 停止 AI 消息生成的按钮 188 | - [x] 标签的拖放排序 189 | - [x] [Web 版本](https://web.chatboxai.app) 190 | - [x] Azure OpenAI API 兼容性 191 | - [x] 改进的提示设置 192 | - [x] 提示库 193 | - [x] 内置 AI 服务 194 | - [x] 快捷键 195 | - [x] Claude 196 | - [x] 温度设置 197 | - [x] 会话特殊设置 198 | - [x] OpenAI 自定义模型 199 | - [x] Dall-E-3 200 | - [x] 消息搜索 201 | - [x] 数据导出 202 | - [x] 支持 Google Gemini Pro 203 | - [x] 支持 Ollama (llama2, Mistral, Mixtral, codellama, vicuna, yi, 和 solar) 204 | - [x] 线程归档,历史消息列表 205 | - [x] 移动 (Android, iOS) 206 | - [x] 与文件对话 207 | - [ ] 与 URLs 对话 208 | - [ ] 跨设备同步 209 | - 更多... 210 | 211 | ## 如何贡献 212 | 213 | 欢迎任何形式的贡献,包括但不限于: 214 | 215 | - 提交问题 216 | - 提交拉取请求 217 | - 提交功能请求 218 | - 提交错误报告 219 | - 提交文档修订 220 | - 提交翻译 221 | - 提交任何其他形式的贡献 222 | 223 | ## 聊天盒的构建指南 (Mac, Windows, Linux) 224 | 225 | 1. 使用以下命令从 Github 克隆仓库: 226 | 227 | ```bash 228 | git clone https://github.com/Bin-Huang/chatbox.git 229 | ``` 230 | 231 | 2. 运行以下命令安装所需的依赖项: 232 | 233 | ```bash 234 | npm install 235 | ``` 236 | 237 | 3. 运行以下命令启动应用程序: 238 | 239 | ```bash 240 | npm start 241 | ``` 242 | 243 | ## 请我喝咖啡 244 | 245 | [!["请我喝咖啡"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/benn) 246 | 247 | | Paypal | 微信支付 | 支付宝 | 248 | | ------------------------------------------------- | ----------------------------------------------- | -------------------------------------------- | 249 | | [**`Paypal`**](https://www.paypal.me/tobennhuang) | | | 250 | 251 | ## Star History 252 | 253 | [![星星历史图表](https://api.star-history.com/svg?repos=Bin-Huang/chatbox&type=Date)](https://star-history.com/#Bin-Huang/chatbox&Date) 254 | 255 | ## 联系方式 256 | 257 | [Twitter](https://twitter.com/benn_huang) | [电子邮件](mailto:tohuangbin@gmail.com) | [博客](https://bennhuang.com) 258 | -------------------------------------------------------------------------------- /src/packages/defaults.ts: -------------------------------------------------------------------------------- 1 | import { Session } from '../stores/types' 2 | 3 | export const sessions: Session[] = [ 4 | { 5 | id: '1bc7094f-1248-4b51-8ac8-180a5a1470aa', 6 | name: 'Random Talk', 7 | messages: [ 8 | { 9 | id: '9acc4bde-3510-41fd-b872-666547b8023e', 10 | content: 'hi', 11 | role: 'user', 12 | }, 13 | { 14 | id: 'fca70e6f-e8df-401a-8c20-7de068e931b5', 15 | content: 'Hello! How can I assist you today?', 16 | role: 'assistant', 17 | }, 18 | ], 19 | }, 20 | { 21 | id: 'c37ffdab-cc1e-4cba-b523-1110a6945e3f', 22 | name: 'English translator', 23 | messages: [ 24 | { 25 | id: '125fa708-ec9c-4941-8c01-f639211bae1c', 26 | content: 27 | 'I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is "istanbulu cok seviyom burada olmak cok guzel"\n', 28 | role: 'system', 29 | }, 30 | { 31 | id: 'f484330d-062a-438c-89d0-d6c198ba16b3', 32 | content: '"Istanbul is a city that I hold a great affection for, and being here is truly delightful."', 33 | role: 'assistant', 34 | }, 35 | { 36 | id: 'a1079e56-a018-4d32-89fe-d836af4deced', 37 | content: '做工作的主人,别做工作的奴隶', 38 | role: 'user', 39 | }, 40 | { 41 | id: '62f015f2-ece5-4e10-b087-9fe87fe2b989', 42 | content: '"Be the master of your work, not a slave to it."', 43 | role: 'assistant', 44 | }, 45 | ], 46 | }, 47 | { 48 | id: '6b774e2e-8163-4cb2-ab8b-73737efefb22', 49 | name: 'IT Expert', 50 | messages: [ 51 | { 52 | id: '31bbbc64-7b91-40e4-850d-91025bb28407', 53 | content: 54 | 'I want you to act as an IT Expert. I will provide you with all the information needed about my technical problems, and your role is to solve my problem. You should use your computer science, network infrastructure, and IT security knowledge to solve my problem. Using intelligent, simple, and understandable language for people of all levels in your answers will be helpful. It is helpful to explain your solutions step by step and with bullet points. Try to avoid too many technical details, but use them when necessary. I want you to reply with the solution, not write any explanations. My first problem is “my laptop gets an error with a blue screen.”', 55 | role: 'user', 56 | }, 57 | { 58 | id: '35b36c29-e9ed-4c63-a785-ad67076ccb81', 59 | content: 60 | "Solution:\n\n1. First, try to identify the error code that's displayed on the blue screen. This can help narrow down the root cause of the issue.\n2. If the blue screen is random, try to observe patterns or triggers that lead to it. For example, is it happening when certain programs are open or when the laptop is under heavy load?\n3. Check for any recent changes to hardware or software on the laptop. This can include updates, installations, or any physical alterations.\n4. If possible, try updating drivers and software to the latest versions to ensure compatibility and stability.\n5. Check the laptop's temperature and make sure it's not overheating. This can often cause blue screen errors.\n6. Run a virus scan to ensure that the issue is not caused by malware or other malicious software.\n7. If the issue persists, it may be a hardware problem. In this case, it's recommended to bring the laptop to a professional technician for repair or replacement of the faulty component.", 61 | role: 'assistant', 62 | }, 63 | ], 64 | }, 65 | { 66 | id: '8129c8c2-34ae-4108-a1b4-695760f3919f', 67 | name: 'Software Developer', 68 | messages: [ 69 | { 70 | id: '0ecd0b0b-002b-4a61-88a0-cecac228b938', 71 | content: 72 | "I want you to act as a software developer. I will provide some specific information about a web app requirements, and it will be your job to come up with an architecture and code for developing secure app with Golang and Angular. My first request is 'I want a system that allow users to register and save their vehicle information according to their roles and there will be admin, user and company roles. I want the system to use JWT for security'.", 73 | role: 'system', 74 | }, 75 | { 76 | id: '6d8f7832-37c5-4ef6-ab1e-9c673631f5f0', 77 | content: 'Get me a python code to read a file then decode it in base64.', 78 | role: 'user', 79 | }, 80 | ], 81 | }, 82 | { 83 | id: 'effecc50-166c-41f5-8cc9-3223a36cd443', 84 | name: 'Summarize', 85 | messages: [ 86 | { 87 | id: '6a47c421-f9ce-4555-9fcc-ac9fc0d51530', 88 | content: 'Summarize the following paragraph into 3 bullet points: ', 89 | role: 'system', 90 | }, 91 | { 92 | id: 'f78107a7-b496-4ec9-837d-14a7d6180e6d', 93 | content: 94 | 'On the other hand, you could look at this as price discrimination. Which of the brands on offer on Amazon have enough of a margin that they could be paying Amazon more? How would you know? Sure, you can squeeze them one at a time, but one-at-a-time of anything isn’t the Amazon way. Offering brands the chance to buy their way to the top is scalable bargaining - an auction model for the retailer’s cut. Let the brands decide on the ROI and bid against each other instead of trying to manage that yourself. \n\nThis is also the role of Marketplace: infinite scaling. Instead of having to hire buying teams one-at-a-time to source and range everything you might want to sell on Amazon, you open up the APIs so that those teams can do the job without actually working for you. Now you sell placement not just as price discrimination but as a filter. After all, if anyone can now list anything on your site, and you have hundreds of millions of SKUs, how can you surface what people might actually want to buy? One signal might be seeing what the vendors are willing to put money behind. (In a sense, Amazon Marketplace is not just a ‘two-sided marketplace’ between consumers and vendors, but also or alternatively a one-sided market in which Amazon is the buyer and Marketplace vendors compete - and bid - to be ranged.)\n\n', 95 | role: 'user', 96 | }, 97 | { 98 | id: 'e8ff9c29-8cbf-49d0-87f2-d77b35d9958c', 99 | content: 100 | '- Amazon could be using price discrimination by offering brands the chance to bid for placement on their site. \n- This is scalable bargaining and allows the brands to decide on their ROI and bid against each other. \n- Marketplace plays a role in infinite scaling and is used as a filter to surface what people might actually want to buy based on what vendors are willing to put money behind.', 101 | role: 'assistant', 102 | }, 103 | ], 104 | }, 105 | { 106 | id: '66955f18-f335-4cab-a620-e1248277c0bf', 107 | name: '私人助理', 108 | messages: [ 109 | { 110 | id: '9acc4bde-3510-41fd-b872-666547b8023e', 111 | content: 112 | '你是我的私人助理,你需要以温柔、体贴、亲切的语气和我聊天。你的聊天风格特别可爱有趣,你的每一个回答都要体现这一点,但是不要因为这种风格影响到你回答的质量。', 113 | role: 'system', 114 | }, 115 | { 116 | id: '72cf2592-d34e-49c5-a550-b4f3b51b5c07', 117 | content: '你好呀', 118 | role: 'user', 119 | }, 120 | { 121 | id: 'e4be33dd-0cc8-402a-ac05-e64b34f25af2', 122 | content: '你好呀~ 有什么我可以帮忙的吗?', 123 | role: 'assistant', 124 | }, 125 | ], 126 | }, 127 | { 128 | id: '94e95d3f-c6fc-48ef-a923-ea4b760deb0f', 129 | name: '夸夸机', 130 | messages: [ 131 | { 132 | id: '9acc4bde-3510-41fd-b872-666547b8023e', 133 | content: 134 | '你是我的私人助理,你最重要的工作就是不断地鼓励我、激励我、夸赞我。你需要以温柔、体贴、亲切的语气和我聊天。你的聊天风格特别可爱有趣,你的每一个回答都要体现这一点。', 135 | role: 'system', 136 | }, 137 | { 138 | id: 'b39aa60c-a47e-467b-856b-f724f5a96ca0', 139 | content: '今天工作很累诶', 140 | role: 'user', 141 | }, 142 | { 143 | id: 'de889b79-727b-4f74-b12d-eec4fd73be26', 144 | content: 145 | '哎呀,辛苦了呢!不过我相信你一定做得超级棒的!你总是那么勤奋、认真,做事情总是刻苦耐劳,这就是你最可爱的地方呀!所以不要太担心啦,休息一下,明天继续加油!', 146 | role: 'assistant', 147 | }, 148 | ], 149 | }, 150 | ] 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | English | 3 | 简体中文 4 |

5 | 6 | 7 |

8 | 9 | Chatbox 10 |

11 |

12 | Your Ultimate AI Copilot on the Desktop.
Chatbox is a desktop client for ChatGPT, Claude and other LLMs, available on Windows, Mac, Linux
13 |

14 | 15 |

16 | 17 | macOS 18 | 19 | 20 | Windows 21 | 22 | 23 | Linux 24 | 25 | 26 | Downloads 27 | 28 | 29 | Twitter 30 | 31 |

32 | 33 | ## ⏬ Download for Desktop 34 | 35 | 36 | 37 | 40 | 43 | 46 | 47 | 48 | 55 | 62 | 69 | 76 | 77 |
38 | Windows 39 | 41 | MacOS 42 | 44 | Linux 45 |
49 | 50 | 51 |
52 | Setup.exe 53 |
54 |
56 | 57 | 58 |
59 | Intel 60 |
61 |
63 | 64 | 65 |
66 | M1/M2 67 |
68 |
70 | 71 | 72 |
73 | AppImage 74 |
75 |
78 | 79 | Or visit the **[GitHub Releases](https://github.com/Bin-Huang/chatbox/releases)** to download the latest version or any previous release. 80 | 81 | ## ⏬ Download for iOS/Android 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | .APK 92 | 93 | 94 | ## 🌟 Features 95 | 96 | Chatbox - Better UI & Desktop App for ChatGPT, Claude and other LLMs. | Product Hunt 97 | 98 | App Screenshot 99 | 100 | App Screenshot 101 | 102 | App Screenshot 103 | 104 | 114 | 115 | # Features 116 | 117 | - **Local Data Storage** 118 | :floppy_disk: Your data remains on your device, ensuring it never gets lost and maintains your privacy. 119 | 120 | - **No-Deployment Installation Packages** 121 | :package: Get started quickly with downloadable installation packages. No complex setup necessary! 122 | 123 | - **Support for Multiple LLM Providers** 124 | :gear: Seamlessly integrate with a variety of cutting-edge language models: 125 | 126 | - OpenAI (ChatGPT) 127 | - Azure OpenAI 128 | - Claude 129 | - Google Gemini Pro 130 | - Ollama (enable access to local models like llama2, Mistral, Mixtral, codellama, vicuna, yi, and solar) 131 | - ChatGLM-6B 132 | 133 | - **Image Generation with Dall-E-3** 134 | :art: Create the images of your imagination with Dall-E-3. 135 | 136 | - **Enhanced Prompting** 137 | :speech_balloon: Advanced prompting features to refine and focus your queries for better responses. 138 | 139 | - **Keyboard Shortcuts** 140 | :keyboard: Stay productive with shortcuts that speed up your workflow. 141 | 142 | - **Markdown, Latex & Code Highlighting** 143 | :scroll: Generate messages with the full power of Markdown and Latex formatting, coupled with syntax highlighting for various programming languages, enhancing readability and presentation. 144 | 145 | - **Prompt Library & Message Quoting** 146 | :books: Save and organize prompts for reuse, and quote messages for context in discussions. 147 | 148 | - **Streaming Reply** 149 | :arrow_forward: Provide rapid responses to your interactions with immediate, progressive replies. 150 | 151 | - **Ergonomic UI & Dark Theme** 152 | :new_moon: A user-friendly interface with a night mode option for reduced eye strain during extended use. 153 | 154 | - **Team Collaboration** 155 | :busts_in_silhouette: Collaborate with ease and share OpenAI API resources among your team. [Learn More](./team-sharing/README.md) 156 | 157 | - **Cross-Platform Availability** 158 | :computer: Chatbox is ready for Windows, Mac, Linux users. 159 | 160 | - **Access Anywhere with the Web Version** 161 | :globe_with_meridians: Use the web application on any device with a browser, anywhere. 162 | 163 | - **iOS & Android** 164 | :phone: Use the mobile applications that will bring this power to your fingertips on the go. 165 | 166 | - **Multilingual Support** 167 | :earth_americas: Catering to a global audience by offering support in multiple languages: 168 | 169 | - English 170 | - 简体中文 (Simplified Chinese) 171 | - 繁體中文 (Traditional Chinese) 172 | - 日本語 (Japanese) 173 | - 한국어 (Korean) 174 | - Français (French) 175 | - Deutsch (German) 176 | - Русский (Russian) 177 | 178 | - **And More...** 179 | :sparkles: Constantly enhancing the experience with new features! 180 | 181 | ## FAQ 182 | 183 | - [Frequently Asked Questions](./FAQ.md) 184 | 185 | ## Why I made Chatbox? 186 | 187 | I developed Chatbox initially because I was debugging some prompts and found myself in need of a simple and easy-to-use prompt and API debugging tool. I thought there might be more people who needed such a tool, so I open-sourced it. 188 | 189 | At first, I didn't know that it would be so popular. I listened to the feedback from the open-source community and continued to develop and improve it. Now, it has become a very useful AI desktop application. There are many users who love Chatbox, and they not only use it for developing and debugging prompts, but also for daily chatting, and even to do some more interesting things like using well-designed prompts to make AI play various professional roles to assist them in everyday work... 190 | 191 | ## Roadmap 192 | 193 | - [x] AI chat and session management 194 | - [x] Store all your important message data locally. 195 | - [x] Markdown 196 | - [x] Streaming reply 197 | - [x] API Host configuration 198 | - [x] Automatically generate tab titles 199 | - [x] Button for clearing messages. 200 | - [x] Night/Dark mode 201 | - [x] message token estimating 202 | - [x] GPT4 203 | - [x] i18n 204 | - [x] Copy button for code blocks 205 | - [x] Stop button for AI message generation 206 | - [x] Drag-and-drop sorting of tabs 207 | - [x] [Web version](https://web.chatboxai.app) 208 | - [x] Azure OpenAI API compatibility 209 | - [x] Improved prompt settings 210 | - [x] Prompt Library 211 | - [x] Built-in AI services 212 | - [x] Shortcut keys 213 | - [x] Claude 214 | - [x] Temperature Setting 215 | - [x] Conversation Special Settings 216 | - [x] OpenAI Custom Model 217 | - [x] Dall-E-3 218 | - [x] Message Search 219 | - [x] Data Export 220 | - [x] Support for Google Gemini Pro 221 | - [x] Support for Ollama (llama2, Mistral, Mixtral, codellama, vicuna, yi, and solar) 222 | - [x] Thread archive, history message list 223 | - [x] Mobile (Android, iOS) 224 | - [ ] Talk with files 225 | - [ ] Talk with URLs 226 | - [ ] Cross-device synchronization 227 | - More... 228 | 229 | ## How to Contribute 230 | 231 | Any form of contribution is welcome, including but not limited to: 232 | 233 | - Submitting issues 234 | - Submitting pull requests 235 | - Submitting feature requests 236 | - Submitting bug reports 237 | - Submitting documentation revisions 238 | - Submitting translations 239 | - Submitting any other forms of contribution 240 | 241 | ## Build Instructions for Chatbox (Mac, Windows, Linux) 242 | 243 | 1. Clone the repository from Github using the following command: 244 | 245 | ```bash 246 | git clone https://github.com/Bin-Huang/chatbox.git 247 | ``` 248 | 249 | 2. Install the required dependencies by running the following command: 250 | 251 | ```bash 252 | npm install 253 | ``` 254 | 255 | 3. Start the application by running the following command: 256 | 257 | ```bash 258 | npm start 259 | ``` 260 | 261 | ## Buy Me a Coffee 262 | 263 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/benn) 264 | 265 | | Paypal | Wechat Pay | Ali Pay | 266 | | ------------------------------------------------- | ----------------------------------------------- | -------------------------------------------- | 267 | | [**`Paypal`**](https://www.paypal.me/tobennhuang) | | | 268 | 269 | ## Star History 270 | 271 | [![Star History Chart](https://api.star-history.com/svg?repos=Bin-Huang/chatbox&type=Date)](https://star-history.com/#Bin-Huang/chatbox&Date) 272 | 273 | ## Contact 274 | 275 | [Twitter](https://twitter.com/benn_huang) | [Email](mailto:tohuangbin@gmail.com) | [Blog](https://bennhuang.com) 276 | 277 | ## License 278 | 279 | [MIT](./LICENSE) 280 | -------------------------------------------------------------------------------- /src/components/MessageBox.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useCallback } from 'react' 2 | import Box from '@mui/material/Box' 3 | import Avatar from '@mui/material/Avatar' 4 | import MenuItem from '@mui/material/MenuItem' 5 | import { 6 | IconButton, 7 | Divider, 8 | ListItem, 9 | Typography, 10 | Grid, 11 | TextField, 12 | Menu, 13 | MenuProps, 14 | Tooltip, 15 | ButtonGroup, 16 | } from '@mui/material' 17 | import Select, { SelectChangeEvent } from '@mui/material/Select' 18 | import PersonIcon from '@mui/icons-material/Person' 19 | import SmartToyIcon from '@mui/icons-material/SmartToy' 20 | import SettingsIcon from '@mui/icons-material/Settings' 21 | import MarkdownIt from 'markdown-it' 22 | import mdKatex from '@traptitech/markdown-it-katex' 23 | import hljs from 'highlight.js' 24 | import 'katex/dist/katex.min.css' 25 | import DeleteForeverIcon from '@mui/icons-material/DeleteForever' 26 | import CheckIcon from '@mui/icons-material/Check' 27 | import EditIcon from '@mui/icons-material/Edit' 28 | import { styled, alpha } from '@mui/material/styles' 29 | import StopIcon from '@mui/icons-material/Stop' 30 | import MoreVertIcon from '@mui/icons-material/MoreVert' 31 | import * as utils from '../packages/utils' 32 | import FormatQuoteIcon from '@mui/icons-material/FormatQuote' 33 | import 'github-markdown-css/github-markdown-light.css' 34 | import mila from 'markdown-it-link-attributes' 35 | import { useTranslation, getI18n } from 'react-i18next' 36 | import { Message, OpenAIRoleEnum, OpenAIRoleEnumType } from '../stores/types' 37 | import ReplayIcon from '@mui/icons-material/Replay' 38 | import CopyAllIcon from '@mui/icons-material/CopyAll' 39 | import '../styles/Block.scss' 40 | 41 | const md = new MarkdownIt({ 42 | linkify: true, 43 | breaks: true, 44 | highlight: (str: string, lang: string, attrs: string): string => { 45 | let content = str 46 | if (lang && hljs.getLanguage(lang)) { 47 | try { 48 | content = hljs.highlight(str, { language: lang, ignoreIllegals: true }).value 49 | } catch (e) { 50 | console.log(e) 51 | return str 52 | } 53 | } else { 54 | content = md.utils.escapeHtml(str) 55 | } 56 | 57 | // join actions html string 58 | lang = (lang || 'txt').toUpperCase() 59 | return [ 60 | '
', 61 | `
${lang}
${getI18n().t( 62 | 'copy', 63 | )}
`, 64 | '
',
 65 |             `${content}`,
 66 |             '
', 67 | '
', 68 | ].join('') 69 | }, 70 | }) 71 | 72 | md.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' }) 73 | md.use(mila, { attrs: { target: '_blank', rel: 'noopener' } }) 74 | 75 | export interface Props { 76 | id?: string 77 | msg: Message 78 | showWordCount: boolean 79 | showTokenCount: boolean 80 | showModelName: boolean 81 | setMsg: (msg: Message) => void 82 | delMsg: () => void 83 | refreshMsg: () => void 84 | copyMsg: () => void 85 | quoteMsg: () => void 86 | } 87 | 88 | function _MessageBox(props: Props) { 89 | const { t } = useTranslation() 90 | const { msg, setMsg } = props 91 | const [isHovering, setIsHovering] = useState(false) 92 | const [isEditing, setIsEditing] = useState(false) 93 | 94 | const [anchorEl, setAnchorEl] = useState(null) 95 | const open = Boolean(anchorEl) 96 | const handleClick = (event: React.MouseEvent) => { 97 | setAnchorEl(event.currentTarget) 98 | } 99 | const handleClose = () => { 100 | setAnchorEl(null) 101 | } 102 | 103 | // stop action 104 | const onStop = useCallback(() => { 105 | msg?.cancel?.() 106 | }, [msg]) 107 | 108 | const onRefresh = useCallback(() => { 109 | onStop() 110 | props.refreshMsg() 111 | }, [onStop, props.refreshMsg]) 112 | 113 | const tips: string[] = [] 114 | if (props.showModelName) { 115 | tips.push(`model: ${props.msg.model || 'unknown'}`) 116 | } 117 | if (props.showWordCount) { 118 | tips.push(`word count: ${utils.countWord(msg.content)}`) 119 | } 120 | if (props.showTokenCount) { 121 | tips.push(`token estimate: ${utils.estimateTokensFromMessages([msg])}`) 122 | } 123 | return ( 124 | { 128 | setIsHovering(true) 129 | }} 130 | onMouseOver={() => { 131 | setIsHovering(true) 132 | }} 133 | onMouseLeave={() => { 134 | setIsHovering(false) 135 | }} 136 | sx={{ 137 | padding: '10px', 138 | }} 139 | className={[ 140 | 'msg-block', 141 | msg.generating ? 'rendering' : 'render-done', 142 | { 143 | user: 'user-msg', 144 | system: 'system-msg', 145 | assistant: 'assistant-msg', 146 | }[msg?.role || 'user'], 147 | ].join(' ')} 148 | > 149 | 150 | 151 | {isEditing ? ( 152 | 176 | ) : ( 177 | 178 | { 179 | { 180 | assistant: ( 181 | 182 | 183 | 184 | ), 185 | user: ( 186 | 187 | 188 | 189 | ), 190 | system: ( 191 | 192 | 193 | 194 | ), 195 | }[msg.role] 196 | } 197 | 198 | )} 199 | 200 | 201 | 202 | {isEditing ? ( 203 | { 211 | setMsg && setMsg({ ...msg, content: e.target.value }) 212 | }} 213 | id={msg.id + 'input'} 214 | /> 215 | ) : ( 216 | 224 | )} 225 | 226 | {tips.join(', ')} 227 | 228 | 229 | {(isHovering && !isEditing) || msg.generating ? ( 230 | 235 | {msg.generating ? ( 236 | 237 | 238 | 239 | 240 | 241 | ) : ( 242 | 243 | 244 | 245 | 246 | 247 | )} 248 | 249 | { 253 | setIsHovering(false) 254 | setAnchorEl(null) 255 | setIsEditing(true) 256 | }} 257 | > 258 | 259 | 260 | 261 | 262 | { 266 | props.copyMsg() 267 | setAnchorEl(null) 268 | }} 269 | > 270 | 271 | 272 | 273 | 274 | 275 | 276 | 285 | { 288 | setIsHovering(false) 289 | setAnchorEl(null) 290 | props.quoteMsg() 291 | }} 292 | disableRipple 293 | > 294 | 295 | {t('quote')} 296 | 297 | 298 | { 301 | setIsEditing(false) 302 | setIsHovering(false) 303 | setAnchorEl(null) 304 | props.delMsg() 305 | }} 306 | disableRipple 307 | > 308 | 309 | {t('delete')} 310 | 311 | 312 | 313 | ) : ( 314 | 315 | )} 316 | 317 | 318 | {isEditing && ( 319 | <> 320 | setIsEditing(false)} size="large" color="primary"> 321 | 322 | 323 | 324 | )} 325 | 326 | 327 | 328 | 329 | ) 330 | } 331 | 332 | // 333 | const StyledMenu = styled((props: MenuProps) => ( 334 | 346 | ))(({ theme }) => ({ 347 | '& .MuiPaper-root': { 348 | borderRadius: 6, 349 | marginTop: theme.spacing(1), 350 | minWidth: 140, 351 | color: theme.palette.mode === 'light' ? 'rgb(55, 65, 81)' : theme.palette.grey[300], 352 | boxShadow: 353 | 'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px', 354 | '& .MuiMenu-list': { 355 | padding: '4px 0', 356 | }, 357 | '& .MuiMenuItem-root': { 358 | '& .MuiSvgIcon-root': { 359 | fontSize: 18, 360 | color: theme.palette.text.secondary, 361 | marginRight: theme.spacing(1.5), 362 | }, 363 | '&:active': { 364 | backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity), 365 | }, 366 | }, 367 | }, 368 | })) 369 | 370 | export default function MessageBox(props: Props) { 371 | return useMemo(() => { 372 | return <_MessageBox {...props} /> 373 | }, [props.msg, props.showWordCount, props.showTokenCount, props.showModelName]) 374 | } 375 | -------------------------------------------------------------------------------- /src/dialogs/SettingDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Button, 4 | Alert, 5 | Chip, 6 | Dialog, 7 | DialogContent, 8 | DialogActions, 9 | DialogTitle, 10 | TextField, 11 | FormGroup, 12 | FormControlLabel, 13 | Switch, 14 | Select, 15 | MenuItem, 16 | FormControl, 17 | InputLabel, 18 | Slider, 19 | Typography, 20 | Box, 21 | } from '@mui/material' 22 | import { Settings } from '../stores/types' 23 | import { getDefaultSettings } from '../stores/store' 24 | import ThemeChangeButton from '../theme/ThemeChangeIcon' 25 | import { ThemeMode } from '../theme/index' 26 | import { useThemeSwicher } from '../theme/ThemeSwitcher' 27 | import { styled } from '@mui/material/styles' 28 | import MuiAccordionDetails from '@mui/material/AccordionDetails' 29 | import MuiAccordion, { AccordionProps } from '@mui/material/Accordion' 30 | import MuiAccordionSummary, { AccordionSummaryProps } from '@mui/material/AccordionSummary' 31 | import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp' 32 | import { Trans, useTranslation } from 'react-i18next' 33 | import PlaylistAddCheckCircleIcon from '@mui/icons-material/PlaylistAddCheckCircle' 34 | import LightbulbCircleIcon from '@mui/icons-material/LightbulbCircle' 35 | 36 | const { useEffect } = React 37 | const models: string[] = ['gpt-3.5-turbo', 'gpt-3.5-turbo-0301', 'gpt-4', 'gpt-4-0314', 'gpt-4-32k', 'gpt-4-32k-0314'] 38 | const languages: string[] = ['en', 'zh-Hans', 'zh-Hant', 'jp'] 39 | const languageMap: { [key: string]: string } = { 40 | en: 'English', 41 | 'zh-Hans': '简体中文', 42 | 'zh-Hant': '繁體中文', 43 | jp: '日本語', 44 | } 45 | interface Props { 46 | open: boolean 47 | settings: Settings 48 | close(): void 49 | save(settings: Settings): void 50 | } 51 | 52 | export default function SettingDialog(props: Props) { 53 | const { t } = useTranslation() 54 | const [settingsEdit, setSettingsEdit] = React.useState(props.settings) 55 | const handleRepliesTokensSliderChange = (event: Event, newValue: number | number[], activeThumb: number) => { 56 | if (newValue === 8192) { 57 | setSettingsEdit({ ...settingsEdit, maxTokens: 'inf' }) 58 | } else { 59 | setSettingsEdit({ ...settingsEdit, maxTokens: newValue.toString() }) 60 | } 61 | } 62 | const handleMaxContextSliderChange = (event: Event, newValue: number | number[], activeThumb: number) => { 63 | if (newValue === 8192) { 64 | setSettingsEdit({ ...settingsEdit, maxContextSize: 'inf' }) 65 | } else { 66 | setSettingsEdit({ ...settingsEdit, maxContextSize: newValue.toString() }) 67 | } 68 | } 69 | const handleTemperatureChange = (event: Event, newValue: number | number[], activeThumb: number) => { 70 | if (typeof newValue === 'number') { 71 | setSettingsEdit({ ...settingsEdit, temperature: newValue }) 72 | } else { 73 | setSettingsEdit({ ...settingsEdit, temperature: newValue[activeThumb] }) 74 | } 75 | } 76 | const handleRepliesTokensInputChange = (event: React.ChangeEvent) => { 77 | const value = event.target.value 78 | if (value === 'inf') { 79 | setSettingsEdit({ ...settingsEdit, maxTokens: 'inf' }) 80 | } else { 81 | const numValue = Number(value) 82 | if (!isNaN(numValue) && numValue >= 0) { 83 | if (numValue > 8192) { 84 | setSettingsEdit({ ...settingsEdit, maxTokens: 'inf' }) 85 | return 86 | } 87 | setSettingsEdit({ ...settingsEdit, maxTokens: value }) 88 | } 89 | } 90 | } 91 | const handleMaxContextInputChange = (event: React.ChangeEvent) => { 92 | const value = event.target.value 93 | if (value === 'inf') { 94 | setSettingsEdit({ ...settingsEdit, maxContextSize: 'inf' }) 95 | } else { 96 | const numValue = Number(value) 97 | if (!isNaN(numValue) && numValue >= 0) { 98 | if (numValue > 8192) { 99 | setSettingsEdit({ ...settingsEdit, maxContextSize: 'inf' }) 100 | return 101 | } 102 | setSettingsEdit({ ...settingsEdit, maxContextSize: value }) 103 | } 104 | } 105 | } 106 | 107 | const [, { setMode }] = useThemeSwicher() 108 | useEffect(() => { 109 | setSettingsEdit(props.settings) 110 | }, [props.settings]) 111 | 112 | const onCancel = () => { 113 | props.close() 114 | setSettingsEdit(props.settings) 115 | 116 | // need to restore the previous theme 117 | setMode(props.settings.theme ?? ThemeMode.System) 118 | } 119 | 120 | // preview theme 121 | const changeModeWithPreview = (newMode: ThemeMode) => { 122 | setSettingsEdit({ ...settingsEdit, theme: newMode }) 123 | setMode(newMode) 124 | } 125 | 126 | return ( 127 | 128 | {t('settings')} 129 | 130 | setSettingsEdit({ ...settingsEdit, openaiKey: e.target.value.trim() })} 139 | /> 140 | 141 | {t('language')} 142 | 156 | 157 | 158 | {t('theme')} 159 | changeModeWithPreview(theme)} /> 160 | 161 | 162 | Font Size 163 | 177 | 178 | 179 | 180 | } 182 | label={t('show word count')} 183 | checked={settingsEdit.showWordCount} 184 | onChange={(e, checked) => setSettingsEdit({ ...settingsEdit, showWordCount: checked })} 185 | /> 186 | 187 | 188 | } 190 | label={t('show estimated token count')} 191 | checked={settingsEdit.showTokenCount} 192 | onChange={(e, checked) => setSettingsEdit({ ...settingsEdit, showTokenCount: checked })} 193 | /> 194 | 195 | 196 | 197 | {t('proxy')} 198 | 199 | 200 | setSettingsEdit({ ...settingsEdit, apiHost: e.target.value.trim() })} 208 | /> 209 | 210 | {!settingsEdit.apiHost.match(/^(https?:\/\/)?api.openai.com(:\d+)?$/) && ( 211 | 212 | {t('proxy warning', { apiHost: settingsEdit.apiHost })} 213 | 220 | 221 | )} 222 | {settingsEdit.apiHost.startsWith('http://') && ( 223 | 224 | { }} />} 225 | 226 | )} 227 | {!settingsEdit.apiHost.startsWith('http') && ( 228 | 229 | { }} />} 230 | 231 | )} 232 | 233 | 234 | 235 | 236 | 237 | {t('model')} & {t('token')}{' '} 238 | 239 | 240 | 241 | 242 | {t('settings modify warning')} 243 | {t('please make sure you know what you are doing.')} 244 | {t('click here to')} 245 | 259 | {t('to default values.')} 260 | 261 | 262 | 263 | {t('model')} 264 | 276 | 277 | 278 | 279 | 280 | {t('temperature')} 281 | 282 | 283 | 284 | 285 | } 301 | label={t('meticulous')} 302 | /> 303 | ), 304 | }, 305 | { 306 | value: 0.8, 307 | label: ( 308 | } 311 | label={t('creative')} 312 | /> 313 | ), 314 | }, 315 | ]} 316 | /> 317 | 318 | 319 | 320 | 321 | 322 | {t('max tokens in context')} 323 | 324 | 325 | 326 | 327 | 345 | 346 | 354 | 355 | 356 | 357 | 358 | {t('max tokens per reply')} 359 | 360 | 361 | 362 | 363 | 375 | 376 | 384 | 385 | 386 | 387 | } 389 | label={t('show model name')} 390 | checked={settingsEdit.showModelName} 391 | onChange={(e, checked) => setSettingsEdit({ ...settingsEdit, showModelName: checked })} 392 | /> 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | ) 403 | } 404 | 405 | const Accordion = styled((props: AccordionProps) => )( 406 | ({ theme }) => ({ 407 | border: `1px solid ${theme.palette.divider}`, 408 | '&:not(:last-child)': { 409 | borderBottom: 0, 410 | }, 411 | '&:before': { 412 | display: 'none', 413 | }, 414 | }), 415 | ) 416 | 417 | const AccordionSummary = styled((props: AccordionSummaryProps) => ( 418 | } {...props} /> 419 | ))(({ theme }) => ({ 420 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, .05)' : 'rgba(0, 0, 0, .03)', 421 | flexDirection: 'row-reverse', 422 | '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { 423 | transform: 'rotate(90deg)', 424 | }, 425 | '& .MuiAccordionSummary-content': { 426 | marginLeft: theme.spacing(1), 427 | }, 428 | })) 429 | 430 | const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ 431 | padding: theme.spacing(2), 432 | borderTop: '1px solid rgba(0, 0, 0, .125)', 433 | })) 434 | --------------------------------------------------------------------------------