├── .eslintignore ├── scripts ├── .gitignore ├── proxychains.template.conf ├── init-proxy.sh ├── delete-deployment-preview.sh ├── setup.sh └── fetch-prompts.mjs ├── .npmrc ├── src-tauri ├── build.rs ├── icons │ ├── icon.ico │ ├── icon.png │ ├── 32x32.png │ ├── icon.icns │ ├── 128x128.png │ ├── StoreLogo.png │ ├── 128x128@2x.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ └── Square310x310Logo.png ├── .gitignore ├── src │ └── main.rs ├── Cargo.toml └── tauri.conf.json ├── vercel.json ├── app ├── components │ ├── mj │ │ ├── index.tsx │ │ ├── mask-editor │ │ │ ├── maskEditor.scss │ │ │ └── utils.ts │ │ ├── mj.module.scss │ │ └── mj-panel.module.scss │ ├── sd │ │ ├── index.tsx │ │ ├── sd-panel.module.scss │ │ └── sd.module.scss │ ├── input-range.module.scss │ ├── auth.module.scss │ ├── artifacts.module.scss │ ├── input-range.tsx │ ├── button.tsx │ ├── settings.module.scss │ ├── message-selector.module.scss │ ├── emoji.tsx │ ├── error.tsx │ ├── button.module.scss │ ├── mask.module.scss │ ├── new-chat.module.scss │ └── auth.tsx ├── icons │ ├── bot.png │ ├── chatgpt.png │ ├── discovery.svg │ ├── image-upload.svg │ ├── left.svg │ ├── down.svg │ ├── share.svg │ ├── paint.svg │ ├── bottom.svg │ ├── history.svg │ ├── dark.svg │ ├── close.svg │ ├── send-white.svg │ ├── rename.svg │ ├── return.svg │ ├── copy.svg │ ├── settings.svg │ ├── menu.svg │ ├── pause.svg │ ├── reload.svg │ ├── add.svg │ ├── three-dots.svg │ ├── sd.svg │ ├── export.svg │ ├── eye.svg │ ├── clear.svg │ ├── prompt.svg │ ├── auto.svg │ ├── loading.svg │ ├── chat.svg │ ├── drag.svg │ ├── edit.svg │ ├── download.svg │ ├── brain.svg │ ├── eye-off.svg │ ├── github.svg │ ├── max.svg │ ├── min.svg │ ├── mask.svg │ ├── image.svg │ ├── confirm.svg │ ├── light.svg │ ├── lightning.svg │ ├── plugin.svg │ ├── cancel.svg │ └── chatgpt.svg ├── store │ └── index.ts ├── masks │ ├── typing.ts │ ├── build.ts │ └── index.ts ├── typing.ts ├── utils │ ├── clone.ts │ ├── merge.ts │ ├── object.ts │ ├── cors.ts │ ├── token.ts │ ├── baidu.ts │ ├── hooks.ts │ ├── cloud │ │ ├── index.ts │ │ └── webdav.ts │ ├── format.ts │ ├── cloudflare.ts │ └── store.ts ├── styles │ ├── animation.scss │ ├── window.scss │ └── highlight.scss ├── page.tsx ├── config │ ├── client.ts │ └── build.ts ├── polyfill.ts ├── api │ ├── config │ │ └── route.ts │ ├── azure │ │ └── [...path] │ │ │ └── route.ts │ ├── upstash │ │ └── [action] │ │ │ └── [...key] │ │ │ └── route.ts │ ├── artifacts │ │ └── route.ts │ ├── openai │ │ └── [...path] │ │ │ └── route.ts │ ├── stability │ │ └── [...path] │ │ │ └── route.ts │ └── mj │ │ └── [...path] │ │ └── route.ts ├── global.d.ts ├── client │ └── controller.ts ├── layout.tsx ├── command.ts └── locales │ └── index.ts ├── public ├── macos.png ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── robots.txt ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── site.webmanifest ├── serviceWorkerRegister.js └── serviceWorker.js ├── .eslintrc.json ├── .husky └── pre-commit ├── docs ├── images │ ├── cover.png │ ├── cover-en.png │ ├── step-1.png │ ├── step-2.png │ ├── step-3.png │ ├── step-4.png │ ├── step-5.png │ ├── step-1-en.png │ ├── step-2-en.png │ ├── step-3-en.png │ ├── step-4-en.png │ └── step-5-en.png ├── translation.md ├── synchronise-chat-logs-cn.md ├── synchronise-chat-logs-ko.md ├── synchronise-chat-logs-ja.md ├── synchronise-chat-logs-en.md ├── synchronise-chat-logs-es.md ├── vercel-cn.md ├── vercel-ja.md ├── cloudflare-pages-ko.md ├── vercel-ko.md ├── cloudflare-pages-cn.md ├── cloudflare-pages-ja.md ├── cloudflare-pages-en.md ├── cloudflare-pages-es.md ├── vercel-es.md └── user-manual-cn.md ├── .lintstagedrc.json ├── .prettierrc.js ├── .babelrc ├── .github ├── ISSUE_TEMPLATE │ ├── 功能建议.md │ ├── 反馈问题.md │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── release.yml │ └── docker.yml ├── dependabot.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitpod.yml ├── .gitignore ├── tsconfig.json ├── LICENSE ├── docker-compose.yml ├── .dockerignore ├── Dockerfile ├── .env.template ├── package.json └── next.config.mjs /.eslintignore: -------------------------------------------------------------------------------- 1 | public/serviceWorker.js -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | proxychains.conf 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com/ 2 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/components/mj/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./mj"; 2 | export * from "./mj-panel"; 3 | -------------------------------------------------------------------------------- /app/components/sd/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./sd"; 2 | export * from "./sd-panel"; 3 | -------------------------------------------------------------------------------- /public/macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/public/macos.png -------------------------------------------------------------------------------- /app/icons/bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/app/icons/bot.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "plugins": ["prettier"] 4 | } 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /app/icons/chatgpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/app/icons/chatgpt.png -------------------------------------------------------------------------------- /docs/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/cover.png -------------------------------------------------------------------------------- /docs/images/cover-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/cover-en.png -------------------------------------------------------------------------------- /docs/images/step-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/step-1.png -------------------------------------------------------------------------------- /docs/images/step-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/step-2.png -------------------------------------------------------------------------------- /docs/images/step-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/step-3.png -------------------------------------------------------------------------------- /docs/images/step-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/step-4.png -------------------------------------------------------------------------------- /docs/images/step-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/step-5.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | User-agent: vitals.vercel-insights.com 4 | Allow: / -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /docs/images/step-1-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/step-1-en.png -------------------------------------------------------------------------------- /docs/images/step-2-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/step-2-en.png -------------------------------------------------------------------------------- /docs/images/step-3-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/step-3-en.png -------------------------------------------------------------------------------- /docs/images/step-4-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/step-4-en.png -------------------------------------------------------------------------------- /docs/images/step-5-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/docs/images/step-5-en.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatAnyTeam/ChatAny/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /app/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./chat"; 2 | export * from "./update"; 3 | export * from "./access"; 4 | export * from "./config"; 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [ 3 | "eslint --fix", 4 | "prettier --write" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: false, 7 | trailingComma: 'all', 8 | bracketSpacing: true, 9 | arrowParens: 'always', 10 | }; 11 | -------------------------------------------------------------------------------- /app/masks/typing.ts: -------------------------------------------------------------------------------- 1 | import { ModelConfig } from "../store"; 2 | import { type Mask } from "../store/mask"; 3 | 4 | export type BuiltinMask = Omit & { 5 | builtin: Boolean; 6 | modelConfig: Partial; 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/proxychains.template.conf: -------------------------------------------------------------------------------- 1 | strict_chain 2 | proxy_dns 3 | 4 | remote_dns_subnet 224 5 | 6 | tcp_read_time_out 15000 7 | tcp_connect_time_out 8000 8 | 9 | localnet 127.0.0.0/255.0.0.0 10 | 11 | [ProxyList] 12 | socks4 127.0.0.1 9050 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "next/babel", 5 | { 6 | "preset-env": { 7 | "targets": { 8 | "browsers": ["> 0.25%, not dead"] 9 | } 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /scripts/init-proxy.sh: -------------------------------------------------------------------------------- 1 | dir="$(dirname "$0")" 2 | config=$dir/proxychains.conf 3 | host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //') 4 | echo "proxying to $host_ip" 5 | cp $dir/proxychains.template.conf $config 6 | sed -i "\$s/.*/http $host_ip 7890/" $config 7 | -------------------------------------------------------------------------------- /app/typing.ts: -------------------------------------------------------------------------------- 1 | export type Updater = (updater: (value: T) => void) => void; 2 | 3 | export const ROLES = ["system", "user", "assistant"] as const; 4 | export type MessageRole = (typeof ROLES)[number]; 5 | 6 | export interface RequestMessage { 7 | role: MessageRole; 8 | content: string; 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/功能建议.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能建议 3 | about: 请告诉我们你的灵光一闪 4 | title: "[Feature] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | **这个功能与现有的问题有关吗?** 10 | 如果有关,请在此列出链接或者描述问题。 11 | 12 | **你想要什么功能或者有什么建议?** 13 | 尽管告诉我们。 14 | 15 | **有没有可以参考的同类竞品?** 16 | 可以给出参考产品的链接或者截图。 17 | 18 | **其他信息** 19 | 可以说说你的其他考虑。 20 | -------------------------------------------------------------------------------- /app/utils/clone.ts: -------------------------------------------------------------------------------- 1 | export function deepClone(obj: T) { 2 | return JSON.parse(JSON.stringify(obj)); 3 | } 4 | 5 | export function ensure( 6 | obj: T, 7 | keys: Array<[keyof T][number]>, 8 | ) { 9 | return keys.every( 10 | (k) => obj[k] !== undefined && obj[k] !== null && obj[k] !== "", 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/components/input-range.module.scss: -------------------------------------------------------------------------------- 1 | .input-range { 2 | border: var(--border-in-light); 3 | border-radius: 10px; 4 | padding: 5px 10px 5px 10px; 5 | font-size: 12px; 6 | display: flex; 7 | justify-content: space-between; 8 | max-width: 40%; 9 | 10 | input[type="range"] { 11 | max-width: calc(100% - 34px); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | tauri::Builder::default() 6 | .plugin(tauri_plugin_window_state::Builder::default().build()) 7 | .run(tauri::generate_context!()) 8 | .expect("error while running tauri application"); 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | build: 10 | permissions: write-all 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Auto Release 14 | uses: "marvinpinto/action-automatic-releases@latest" 15 | with: 16 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 17 | draft: true -------------------------------------------------------------------------------- /app/icons/discovery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/styles/animation.scss: -------------------------------------------------------------------------------- 1 | @keyframes slide-in { 2 | from { 3 | opacity: 0; 4 | transform: translateY(20px); 5 | } 6 | 7 | to { 8 | opacity: 1; 9 | transform: translateY(0px); 10 | } 11 | } 12 | 13 | @keyframes slide-in-from-top { 14 | from { 15 | opacity: 0; 16 | transform: translateY(-20px); 17 | } 18 | 19 | to { 20 | opacity: 1; 21 | transform: translateY(0px); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from "@vercel/analytics/react"; 2 | 3 | import { Home } from "./components/home"; 4 | 5 | import { getServerSideConfig } from "./config/server"; 6 | 7 | const serverConfig = getServerSideConfig(); 8 | 9 | export default async function App() { 10 | return ( 11 | <> 12 | 13 | {serverConfig?.isVercel && ( 14 | <> 15 | 16 | 17 | )} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/utils/merge.ts: -------------------------------------------------------------------------------- 1 | export function merge(target: any, source: any) { 2 | Object.keys(source).forEach(function (key) { 3 | if ( 4 | source.hasOwnProperty(key) && // Check if the property is not inherited 5 | source[key] && 6 | typeof source[key] === "object" || key === "__proto__" || key === "constructor" 7 | ) { 8 | merge((target[key] = target[key] || {}), source[key]); 9 | return; 10 | } 11 | target[key] = source[key]; 12 | }); 13 | } -------------------------------------------------------------------------------- /app/utils/object.ts: -------------------------------------------------------------------------------- 1 | export function omit( 2 | obj: T, 3 | ...keys: U 4 | ): Omit { 5 | const ret: any = { ...obj }; 6 | keys.forEach((key) => delete ret[key]); 7 | return ret; 8 | } 9 | 10 | export function pick( 11 | obj: T, 12 | ...keys: U 13 | ): Pick { 14 | const ret: any = {}; 15 | keys.forEach((key) => (ret[key] = obj[key])); 16 | return ret; 17 | } 18 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 6 | 7 | tasks: 8 | - init: yarn install && yarn run dev 9 | command: yarn run dev 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/icons/image-upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NextChat", 3 | "short_name": "NextChat", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/", 17 | "theme_color": "#ffffff", 18 | "background_color": "#ffffff", 19 | "display": "standalone" 20 | } -------------------------------------------------------------------------------- /app/utils/cors.ts: -------------------------------------------------------------------------------- 1 | import { getClientConfig } from "../config/client"; 2 | import { ApiPath, DEFAULT_API_HOST } from "../constant"; 3 | 4 | export function corsPath(path: string) { 5 | const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_API_HOST}` : ""; 6 | 7 | if (baseUrl === "" && path === "") { 8 | return ""; 9 | } 10 | if (!path.startsWith("/")) { 11 | path = "/" + path; 12 | } 13 | 14 | if (!path.endsWith("/")) { 15 | path += "/"; 16 | } 17 | 18 | return `${baseUrl}${path}`; 19 | } 20 | -------------------------------------------------------------------------------- /app/icons/left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/translation.md: -------------------------------------------------------------------------------- 1 | # How to add a new translation? 2 | 3 | Assume that we are adding a new translation for `new`. 4 | 5 | 1. copy `app/locales/en.ts` to `app/locales/new.ts`; 6 | 2. edit `new.ts`, change `const en: LocaleType = ` to `const new: PartialLocaleType`, and `export default new;`; 7 | 3. edit `app/locales/index.ts`: 8 | 4. `import new from './new.ts'`; 9 | 5. add `new` to `ALL_LANGS`; 10 | 6. add `new: "new lang"` to `ALL_LANG_OPTIONS`; 11 | 7. translate the strings in `new.ts`; 12 | 8. submit a pull request, and the author will merge it. 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /app/utils/token.ts: -------------------------------------------------------------------------------- 1 | export function estimateTokenLength(input: string): number { 2 | let tokenLength = 0; 3 | 4 | for (let i = 0; i < input.length; i++) { 5 | const charCode = input.charCodeAt(i); 6 | 7 | if (charCode < 128) { 8 | // ASCII character 9 | if (charCode <= 122 && charCode >= 65) { 10 | // a-Z 11 | tokenLength += 0.25; 12 | } else { 13 | tokenLength += 0.5; 14 | } 15 | } else { 16 | // Unicode character 17 | tokenLength += 1.5; 18 | } 19 | } 20 | 21 | return tokenLength; 22 | } 23 | -------------------------------------------------------------------------------- /app/utils/baidu.ts: -------------------------------------------------------------------------------- 1 | import { BAIDU_OATUH_URL } from "../constant"; 2 | /** 3 | * 使用 AK,SK 生成鉴权签名(Access Token) 4 | * @return 鉴权签名信息 5 | */ 6 | export async function getAccessToken( 7 | clientId: string, 8 | clientSecret: string, 9 | ): Promise<{ 10 | access_token: string; 11 | expires_in: number; 12 | error?: number; 13 | }> { 14 | const res = await fetch( 15 | `${BAIDU_OATUH_URL}?grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}`, 16 | { 17 | method: "POST", 18 | mode: "cors", 19 | }, 20 | ); 21 | const resJson = await res.json(); 22 | return resJson; 23 | } 24 | -------------------------------------------------------------------------------- /app/icons/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/paint.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/masks/build.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { CN_MASKS } from "./cn"; 4 | import { TW_MASKS } from "./tw"; 5 | import { EN_MASKS } from "./en"; 6 | 7 | import { type BuiltinMask } from "./typing"; 8 | 9 | const BUILTIN_MASKS: Record = { 10 | cn: CN_MASKS, 11 | tw: TW_MASKS, 12 | en: EN_MASKS, 13 | }; 14 | 15 | const dirname = path.dirname(__filename); 16 | 17 | fs.writeFile( 18 | dirname + "/../../public/masks.json", 19 | JSON.stringify(BUILTIN_MASKS, null, 4), 20 | function (error) { 21 | if (error) { 22 | console.error("[Build] failed to build masks", error); 23 | } 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/反馈问题.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 反馈问题 3 | about: 请告诉我们你遇到的问题 4 | title: "[Bug] 请输入问题标题" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | **反馈须知** 10 | 11 | ⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭,如果没有提供下方的信息,我们无法定位你的问题。 12 | 13 | 请在下方中括号内输入 x 来表示你已经知晓相关内容。 14 | - [ ] 我确认已经在 [Issues](https://github.com/ChatAnyTeam/ChatAny/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。 15 | 16 | **描述问题** 17 | 请在此描述你遇到了什么问题。 18 | 19 | **如何复现** 20 | 请告诉我们你是通过什么操作触发的该问题。 21 | 22 | **截图** 23 | 请在此提供控制台截图、屏幕截图或者服务端的 log 截图。 24 | 25 | **一些必要的信息** 26 | - 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16] 27 | - 浏览器: [比如 chrome, safari] 28 | - 版本: [填写设置页面的版本号] 29 | - 部署方式:[比如 vercel、docker 或者服务器部署] 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | dev 38 | 39 | .vscode 40 | .idea 41 | 42 | # docker-compose env files 43 | .env 44 | 45 | *.key 46 | *.key.pub 47 | 48 | masks.json 49 | -------------------------------------------------------------------------------- /app/icons/bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/auth.module.scss: -------------------------------------------------------------------------------- 1 | .auth-page { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 100%; 6 | width: 100%; 7 | flex-direction: column; 8 | 9 | .auth-logo { 10 | transform: scale(1.4); 11 | } 12 | 13 | .auth-title { 14 | font-size: 24px; 15 | font-weight: bold; 16 | line-height: 2; 17 | } 18 | 19 | .auth-tips { 20 | font-size: 14px; 21 | } 22 | 23 | .auth-input { 24 | margin: 3vh 0; 25 | } 26 | 27 | .auth-actions { 28 | display: flex; 29 | justify-content: center; 30 | flex-direction: column; 31 | 32 | button:not(:last-child) { 33 | margin-bottom: 10px; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/icons/history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | -------------------------------------------------------------------------------- /app/components/artifacts.module.scss: -------------------------------------------------------------------------------- 1 | .artifacts { 2 | display: flex; 3 | width: 100%; 4 | height: 100%; 5 | flex-direction: column; 6 | &-header { 7 | display: flex; 8 | align-items: center; 9 | height: 36px; 10 | padding: 20px; 11 | background: var(--second); 12 | } 13 | &-title { 14 | flex: 1; 15 | text-align: center; 16 | font-weight: bold; 17 | font-size: 24px; 18 | } 19 | &-content { 20 | flex-grow: 1; 21 | padding: 0 20px 20px 20px; 22 | background-color: var(--second); 23 | } 24 | } 25 | 26 | .artifacts-iframe { 27 | width: 100%; 28 | border: var(--border-in-light); 29 | border-radius: 6px; 30 | background-color: var(--gray); 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /app/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useAccessStore, useAppConfig } from "../store"; 3 | import { collectModelsWithDefaultModel } from "./model"; 4 | 5 | export function useAllModels() { 6 | const accessStore = useAccessStore(); 7 | const configStore = useAppConfig(); 8 | const models = useMemo(() => { 9 | return collectModelsWithDefaultModel( 10 | configStore.models, 11 | [configStore.customModels, accessStore.customModels].join(","), 12 | accessStore.defaultModel, 13 | ); 14 | }, [ 15 | accessStore.customModels, 16 | accessStore.defaultModel, 17 | configStore.customModels, 18 | configStore.models, 19 | ]); 20 | 21 | return models; 22 | } 23 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-cn.md: -------------------------------------------------------------------------------- 1 | # 同步聊天记录 2 | ## 准备工作 3 | - GitHub账号 4 | - 拥有自己搭建过的ChatGPT-Next-Web的服务器 5 | - [UpStash](https://upstash.com) 6 | 7 | ## 开始教程 8 | 1. 注册UpStash账号 9 | 2. 创建数据库 10 | 11 | ![注册登录](./images/upstash-1.png) 12 | 13 | ![创建数据库](./images/upstash-2.png) 14 | 15 | ![选择服务器](./images/upstash-3.png) 16 | 17 | 3. 找到REST API,分别复制UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN(⚠切记⚠:不要泄露Token!) 18 | 19 | ![复制](./images/upstash-4.png) 20 | 21 | 4. UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN复制到你的同步配置,点击**检查可用性** 22 | 23 | ![同步1](./images/upstash-5.png) 24 | 25 | 如果没什么问题,那就成功了 26 | 27 | ![同步可用性完成的样子](./images/upstash-6.png) 28 | 29 | 5. Success! 30 | 31 | ![好耶~!](./images/upstash-7.png) 32 | -------------------------------------------------------------------------------- /app/config/client.ts: -------------------------------------------------------------------------------- 1 | import { BuildConfig, getBuildConfig } from "./build"; 2 | 3 | export function getClientConfig() { 4 | if (typeof document !== "undefined") { 5 | // client side 6 | return JSON.parse(queryMeta("config") || "{}") as BuildConfig; 7 | } 8 | 9 | if (typeof process !== "undefined") { 10 | // server side 11 | return getBuildConfig(); 12 | } 13 | } 14 | 15 | function queryMeta(key: string, defaultValue?: string): string { 16 | let ret: string; 17 | if (document) { 18 | const meta = document.head.querySelector( 19 | `meta[name='${key}']`, 20 | ) as HTMLMetaElement; 21 | ret = meta?.content ?? ""; 22 | } else { 23 | ret = defaultValue ?? ""; 24 | } 25 | 26 | return ret; 27 | } 28 | -------------------------------------------------------------------------------- /app/polyfill.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Array { 3 | at(index: number): T | undefined; 4 | } 5 | } 6 | 7 | if (!Array.prototype.at) { 8 | Array.prototype.at = function (index: number) { 9 | // Get the length of the array 10 | const length = this.length; 11 | 12 | // Convert negative index to a positive index 13 | if (index < 0) { 14 | index = length + index; 15 | } 16 | 17 | // Return undefined if the index is out of range 18 | if (index < 0 || index >= length) { 19 | return undefined; 20 | } 21 | 22 | // Use Array.prototype.slice method to get value at the specified index 23 | return Array.prototype.slice.call(this, index, index + 1)[0]; 24 | }; 25 | } 26 | 27 | export {}; 28 | -------------------------------------------------------------------------------- /app/components/mj/mask-editor/maskEditor.scss: -------------------------------------------------------------------------------- 1 | $grid-size: 10px; 2 | 3 | .react-mask-editor-outer { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | align-items: stretch; 8 | 9 | .react-mask-editor-inner { 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: flex-start; 13 | align-items: flex-start; 14 | position: relative; 15 | overflow: auto; 16 | flex: 1 1 auto; 17 | 18 | .all-canvases { 19 | position: relative; 20 | } 21 | } 22 | 23 | canvas { 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | } 28 | 29 | .mask-canvas { 30 | z-index: 10; 31 | } 32 | 33 | .cursor-canvas { 34 | z-index: 20; 35 | background-color: transparent; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 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 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/calcTextareaHeight.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /app/styles/window.scss: -------------------------------------------------------------------------------- 1 | .window-header { 2 | padding: 14px 20px; 3 | border-bottom: rgba(0, 0, 0, 0.1) 1px solid; 4 | position: relative; 5 | 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | } 10 | 11 | .window-header-title { 12 | max-width: calc(100% - 100px); 13 | overflow: hidden; 14 | 15 | .window-header-main-title { 16 | font-size: 20px; 17 | font-weight: bolder; 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | white-space: nowrap; 21 | display: block; 22 | max-width: 50vw; 23 | } 24 | 25 | .window-header-sub-title { 26 | font-size: 14px; 27 | } 28 | } 29 | 30 | .window-actions { 31 | display: inline-flex; 32 | } 33 | 34 | .window-action-button:not(:first-child) { 35 | margin-left: 10px; 36 | } 37 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-ko.md: -------------------------------------------------------------------------------- 1 | # UpStash를 사용하여 채팅 기록 동기화 2 | ## 사전 준비물 3 | - GitHub 계정 4 | - 자체 ChatGPT-Next-Web 서버 설정 5 | - [UpStash](https://upstash.com) 6 | 7 | ## 시작하기 8 | 1. UpStash 계정 등록 9 | 2. 데이터베이스 생성 10 | 11 | ![등록 및 로그인](./images/upstash-1.png) 12 | 13 | ![데이터베이스 생성](./images/upstash-2.png) 14 | 15 | ![서버 선택](./images/upstash-3.png) 16 | 17 | 3. REST API를 찾아 UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 복사합니다 (⚠주의⚠: 토큰을 공유하지 마십시오!) 18 | 19 | ![복사](./images/upstash-4.png) 20 | 21 | 4. UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 동기화 구성에 복사한 다음 **가용성 확인**을 클릭합니다. 22 | 23 | ![동기화 1](./images/upstash-5.png) 24 | 25 | 모든 것이 정상인 경우,이 단계를 성공적으로 완료했습니다. 26 | 27 | ![동기화 가용성 확인 완료](./images/upstash-6.png) 28 | 29 | 5. 성공! 30 | 31 | ![잘 했어요~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /app/icons/dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-ja.md: -------------------------------------------------------------------------------- 1 | # UpStashを使用してチャットログを同期する 2 | ## 事前準備 3 | - GitHubアカウント 4 | - 自分自身でChatGPT-Next-Webのサーバーをセットアップしていること 5 | - [UpStash](https://upstash.com) 6 | 7 | ## 始める 8 | 1. UpStashアカウントを登録します。 9 | 2. データベースを作成します。 10 | 11 | ![登録とログイン](./images/upstash-1.png) 12 | 13 | ![データベースの作成](./images/upstash-2.png) 14 | 15 | ![サーバーの選択](./images/upstash-3.png) 16 | 17 | 3. REST APIを見つけ、UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENをコピーします(⚠重要⚠:トークンを共有しないでください!) 18 | 19 | ![コピー](./images/upstash-4.png) 20 | 21 | 4. UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENを同期設定にコピーし、次に「可用性を確認」をクリックします。 22 | 23 | ![同期1](./images/upstash-5.png) 24 | 25 | すべてが正常であれば、このステップは成功です。 26 | 27 | ![同期可用性チェックが完了しました](./images/upstash-6.png) 28 | 29 | 5. 成功! 30 | 31 | ![お疲れ様でした~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /app/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/send-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/rename.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/return.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/input-range.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styles from "./input-range.module.scss"; 3 | 4 | interface InputRangeProps { 5 | onChange: React.ChangeEventHandler; 6 | title?: string; 7 | value: number | string; 8 | className?: string; 9 | min: string; 10 | max: string; 11 | step: string; 12 | } 13 | 14 | export function InputRange({ 15 | onChange, 16 | title, 17 | value, 18 | className, 19 | min, 20 | max, 21 | step, 22 | }: InputRangeProps) { 23 | return ( 24 |
25 | {title || value} 26 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/api/config/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import { getServerSideConfig } from "../../config/server"; 4 | 5 | const serverConfig = getServerSideConfig(); 6 | 7 | // Danger! Do not hard code any secret value here! 8 | // 警告!不要在这里写入任何敏感信息! 9 | const DANGER_CONFIG = { 10 | needCode: serverConfig.needCode, 11 | hideUserApiKey: serverConfig.hideUserApiKey, 12 | disableGPT4: serverConfig.disableGPT4, 13 | hideBalanceQuery: serverConfig.hideBalanceQuery, 14 | disableFastLink: serverConfig.disableFastLink, 15 | customModels: serverConfig.customModels, 16 | defaultModel: serverConfig.defaultModel, 17 | }; 18 | 19 | declare global { 20 | type DangerConfig = typeof DANGER_CONFIG; 21 | } 22 | 23 | async function handle() { 24 | return NextResponse.json(DANGER_CONFIG); 25 | } 26 | 27 | export const GET = handle; 28 | export const POST = handle; 29 | 30 | export const runtime = "edge"; 31 | -------------------------------------------------------------------------------- /app/utils/cloud/index.ts: -------------------------------------------------------------------------------- 1 | import { createWebDavClient } from "./webdav"; 2 | import { createUpstashClient } from "./upstash"; 3 | 4 | export enum ProviderType { 5 | WebDAV = "webdav", 6 | UpStash = "upstash", 7 | } 8 | 9 | export const SyncClients = { 10 | [ProviderType.UpStash]: createUpstashClient, 11 | [ProviderType.WebDAV]: createWebDavClient, 12 | } as const; 13 | 14 | type SyncClientConfig = { 15 | [K in keyof typeof SyncClients]: (typeof SyncClients)[K] extends ( 16 | _: infer C, 17 | ) => any 18 | ? C 19 | : never; 20 | }; 21 | 22 | export type SyncClient = { 23 | get: (key: string) => Promise; 24 | set: (key: string, value: string) => Promise; 25 | check: () => Promise; 26 | }; 27 | 28 | export function createSyncClient( 29 | provider: T, 30 | config: SyncClientConfig[T], 31 | ): SyncClient { 32 | return SyncClients[provider](config as any) as any; 33 | } 34 | -------------------------------------------------------------------------------- /app/icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.jpg"; 2 | declare module "*.png"; 3 | declare module "*.woff2"; 4 | declare module "*.woff"; 5 | declare module "*.ttf"; 6 | declare module "*.scss" { 7 | const content: Record; 8 | export default content; 9 | } 10 | 11 | declare module "*.svg"; 12 | 13 | declare interface Window { 14 | __TAURI__?: { 15 | writeText(text: string): Promise; 16 | invoke(command: string, payload?: Record): Promise; 17 | dialog: { 18 | save(options?: Record): Promise; 19 | }; 20 | fs: { 21 | writeBinaryFile(path: string, data: Uint8Array): Promise; 22 | writeTextFile(path: string, data: string): Promise; 23 | }; 24 | notification:{ 25 | requestPermission(): Promise; 26 | isPermissionGranted(): Promise; 27 | sendNotification(options: string | Options): void; 28 | }; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /app/icons/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/utils/format.ts: -------------------------------------------------------------------------------- 1 | export function prettyObject(msg: any) { 2 | const obj = msg; 3 | if (typeof msg !== "string") { 4 | msg = JSON.stringify(msg, null, " "); 5 | } 6 | if (msg === "{}") { 7 | return obj.toString(); 8 | } 9 | if (msg.startsWith("```json")) { 10 | return msg; 11 | } 12 | return ["```json", msg, "```"].join("\n"); 13 | } 14 | 15 | export function* chunks(s: string, maxBytes = 1000 * 1000) { 16 | const decoder = new TextDecoder("utf-8"); 17 | let buf = new TextEncoder().encode(s); 18 | while (buf.length) { 19 | let i = buf.lastIndexOf(32, maxBytes + 1); 20 | // If no space found, try forward search 21 | if (i < 0) i = buf.indexOf(32, maxBytes); 22 | // If there's no space at all, take all 23 | if (i < 0) i = buf.length; 24 | // This is a safe cut-off point; never half-way a multi-byte 25 | yield decoder.decode(buf.slice(0, i)); 26 | buf = buf.slice(i + 1); // Skip space (if any) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-en.md: -------------------------------------------------------------------------------- 1 | # Synchronize Chat Logs with UpStash 2 | ## Prerequisites 3 | - GitHub account 4 | - Your own ChatGPT-Next-Web server set up 5 | - [UpStash](https://upstash.com) 6 | 7 | ## Getting Started 8 | 1. Register for an UpStash account. 9 | 2. Create a database. 10 | 11 | ![Register and Login](./images/upstash-1.png) 12 | 13 | ![Create Database](./images/upstash-2.png) 14 | 15 | ![Select Server](./images/upstash-3.png) 16 | 17 | 3. Find the REST API and copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (⚠Important⚠: Do not share your token!) 18 | 19 | ![Copy](./images/upstash-4.png) 20 | 21 | 4. Copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN into your synchronization configuration, then click **Check Availability**. 22 | 23 | ![Synchronize 1](./images/upstash-5.png) 24 | 25 | If everything is in order, you've successfully completed this step. 26 | 27 | ![Sync Availability Check Completed](./images/upstash-6.png) 28 | 29 | 5. Success! 30 | 31 | ![Great job~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /app/components/sd/sd-panel.module.scss: -------------------------------------------------------------------------------- 1 | .ctrl-param-item { 2 | display: flex; 3 | justify-content: space-between; 4 | min-height: 40px; 5 | padding: 10px 0; 6 | animation: slide-in ease 0.6s; 7 | flex-direction: column; 8 | 9 | .ctrl-param-item-header { 10 | display: flex; 11 | align-items: center; 12 | 13 | .ctrl-param-item-title { 14 | font-size: 14px; 15 | font-weight: bolder; 16 | margin-bottom: 5px; 17 | } 18 | } 19 | 20 | .ctrl-param-item-sub-title { 21 | font-size: 12px; 22 | font-weight: normal; 23 | margin-top: 3px; 24 | } 25 | textarea { 26 | appearance: none; 27 | border-radius: 10px; 28 | border: var(--border-in-light); 29 | min-height: 36px; 30 | box-sizing: border-box; 31 | background: var(--white); 32 | color: var(--black); 33 | padding: 0 10px; 34 | max-width: 50%; 35 | font-family: inherit; 36 | } 37 | } 38 | 39 | .ai-models { 40 | button { 41 | margin-bottom: 10px; 42 | padding: 10px; 43 | width: 100%; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/icons/reload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/controller.ts: -------------------------------------------------------------------------------- 1 | // To store message streaming controller 2 | export const ChatControllerPool = { 3 | controllers: {} as Record, 4 | 5 | addController( 6 | sessionId: string, 7 | messageId: string, 8 | controller: AbortController, 9 | ) { 10 | const key = this.key(sessionId, messageId); 11 | this.controllers[key] = controller; 12 | return key; 13 | }, 14 | 15 | stop(sessionId: string, messageId: string) { 16 | const key = this.key(sessionId, messageId); 17 | const controller = this.controllers[key]; 18 | controller?.abort(); 19 | }, 20 | 21 | stopAll() { 22 | Object.values(this.controllers).forEach((v) => v.abort()); 23 | }, 24 | 25 | hasPending() { 26 | return Object.values(this.controllers).length > 0; 27 | }, 28 | 29 | remove(sessionId: string, messageId: string) { 30 | const key = this.key(sessionId, messageId); 31 | delete this.controllers[key]; 32 | }, 33 | 34 | key(sessionId: string, messageIndex: string) { 35 | return `${sessionId},${messageIndex}`; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /app/icons/three-dots.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/sd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] 输入BUG标题" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Deployment** 27 | - [ ] Docker 28 | - [ ] Vercel 29 | - [ ] Railway 30 | - [ ] Server 31 | 32 | **Desktop (please complete the following information):** 33 | - OS: [e.g. iOS] 34 | - Browser [e.g. chrome, safari] 35 | - Version [e.g. 22] 36 | 37 | **Smartphone (please complete the following information):** 38 | - Device: [e.g. iPhone6] 39 | - OS: [e.g. iOS8.1] 40 | - Browser [e.g. stock browser, safari] 41 | - Version [e.g. 22] 42 | 43 | **Additional Logs** 44 | Add any logs about the problem here. 45 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-es.md: -------------------------------------------------------------------------------- 1 | # Sincronizzare i Log delle Chat con UpStash 2 | ## Prerequisiti 3 | - Account GitHub 4 | - Server ChatGPT-Next-Web di propria configurazione 5 | - [UpStash](https://upstash.com) 6 | 7 | ## Per iniziare 8 | 1. Registrarsi per un account UpStash. 9 | 2. Creare un database. 10 | 11 | ![Registrarsi ed Accedere](./images/upstash-1.png) 12 | 13 | ![Creare un Database](./images/upstash-2.png) 14 | 15 | ![Selezionare il Server](./images/upstash-3.png) 16 | 17 | 3. Trovare l'API REST e copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN (⚠Importante⚠: Non condividere il token!) 18 | 19 | ![Copia](./images/upstash-4.png) 20 | 21 | 4. Copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN nella configurazione di sincronizzazione, quindi fare clic su **Verifica la Disponibilità**. 22 | 23 | ![Sincronizzazione 1](./images/upstash-5.png) 24 | 25 | Se tutto è in ordine, hai completato con successo questa fase. 26 | 27 | ![Verifica la Disponibilità della Sincronizzazione Completata](./images/upstash-6.png) 28 | 29 | 5. Successo! 30 | 31 | ![Ottimo lavoro~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2024 Zhang Yifei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/icons/export.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/clear.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/prompt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/sd/sd.module.scss: -------------------------------------------------------------------------------- 1 | .sd-img-list{ 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: space-between; 5 | .sd-img-item{ 6 | width: 48%; 7 | .sd-img-item-info{ 8 | flex:1; 9 | width: 100%; 10 | overflow: hidden; 11 | user-select: text; 12 | p{ 13 | margin: 6px; 14 | font-size: 12px; 15 | } 16 | .line-1{ 17 | overflow: hidden; 18 | white-space: nowrap; 19 | text-overflow: ellipsis; 20 | } 21 | } 22 | .pre-img{ 23 | display: flex; 24 | width: 130px; 25 | justify-content: center; 26 | align-items: center; 27 | background-color: var(--second); 28 | border-radius: 10px; 29 | } 30 | .img{ 31 | width: 130px; 32 | height: 130px; 33 | border-radius: 10px; 34 | overflow: hidden; 35 | cursor: pointer; 36 | transition: all .3s; 37 | &:hover{ 38 | opacity: .7; 39 | } 40 | } 41 | &:not(:last-child){ 42 | margin-bottom: 20px; 43 | } 44 | } 45 | } 46 | 47 | @media only screen and (max-width: 600px) { 48 | .sd-img-list{ 49 | .sd-img-item{ 50 | width: 100%; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /public/serviceWorkerRegister.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | window.addEventListener('DOMContentLoaded', function () { 3 | navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) { 4 | console.log('ServiceWorker registration successful with scope: ', registration.scope); 5 | const sw = registration.installing || registration.waiting 6 | if (sw) { 7 | sw.onstatechange = function() { 8 | if (sw.state === 'installed') { 9 | // SW installed. Reload for SW intercept serving SW-enabled page. 10 | console.log('ServiceWorker installed reload page'); 11 | window.location.reload(); 12 | } 13 | } 14 | } 15 | registration.update().then(res => { 16 | console.log('ServiceWorker registration update: ', res); 17 | }); 18 | window._SW_ENABLED = true 19 | }, function (err) { 20 | console.error('ServiceWorker registration failed: ', err); 21 | }); 22 | navigator.serviceWorker.addEventListener('controllerchange', function() { 23 | console.log('ServiceWorker controllerchange '); 24 | window.location.reload(true); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /app/components/mj/mask-editor/utils.ts: -------------------------------------------------------------------------------- 1 | export const toMask = (canvas: HTMLCanvasElement) => { 2 | const ctx = canvas.getContext("2d"); 3 | const size = { 4 | x: canvas.width, 5 | y: canvas.height, 6 | }; 7 | const imageData = ctx?.getImageData(0, 0, size.x, size.y); 8 | // @ts-ignore 9 | const origData = Uint8ClampedArray.from(imageData.data); 10 | if (imageData) { 11 | for (var i = 0; i < imageData?.data.length; i += 4) { 12 | const pixelColor = 13 | imageData.data[i] === 255 ? [0, 0, 0] : [255, 255, 255]; 14 | imageData.data[i] = pixelColor[0]; 15 | imageData.data[i + 1] = pixelColor[1]; 16 | imageData.data[i + 2] = pixelColor[2]; 17 | imageData.data[i + 3] = 255; 18 | } 19 | ctx?.putImageData(imageData, 0, 0); 20 | } 21 | 22 | const dataUrl = canvas.toDataURL(); 23 | for (var i = 0; i < (imageData?.data?.length || 0); i++) { 24 | // @ts-ignore 25 | imageData.data[i] = origData[i]; 26 | } 27 | // @ts-ignore 28 | ctx?.putImageData(imageData, 0, 0); 29 | 30 | return dataUrl; 31 | }; 32 | 33 | export const hexToRgb = (color: string) => { 34 | var parts = color.replace("#", "").match(/.{1,2}/g); 35 | return parts?.map((part) => parseInt(part, 16)); 36 | }; 37 | -------------------------------------------------------------------------------- /app/icons/auto.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/loading.svg: -------------------------------------------------------------------------------- 1 | Layer 1 -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### 💻 变更类型 | Change Type 2 | 3 | 4 | 5 | - [ ] feat 6 | - [ ] fix 7 | - [ ] refactor 8 | - [ ] perf 9 | - [ ] style 10 | - [ ] test 11 | - [ ] docs 12 | - [ ] ci 13 | - [ ] chore 14 | - [ ] build 15 | 16 | #### 🔀 变更说明 | Description of Change 17 | 18 | 22 | 23 | #### 📝 补充信息 | Additional Information 24 | 25 | 29 | -------------------------------------------------------------------------------- /app/icons/chat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/masks/index.ts: -------------------------------------------------------------------------------- 1 | import { Mask } from "../store/mask"; 2 | import { CN_MASKS } from "./cn"; 3 | import { TW_MASKS } from "./tw"; 4 | import { EN_MASKS } from "./en"; 5 | 6 | import { type BuiltinMask } from "./typing"; 7 | export { type BuiltinMask } from "./typing"; 8 | 9 | export const BUILTIN_MASK_ID = 100000; 10 | 11 | export const BUILTIN_MASK_STORE = { 12 | buildinId: BUILTIN_MASK_ID, 13 | masks: {} as Record, 14 | get(id?: string) { 15 | if (!id) return undefined; 16 | return this.masks[id] as Mask | undefined; 17 | }, 18 | add(m: BuiltinMask) { 19 | const mask = { ...m, id: this.buildinId++, builtin: true }; 20 | this.masks[mask.id] = mask; 21 | return mask; 22 | }, 23 | }; 24 | 25 | export const BUILTIN_MASKS: BuiltinMask[] = []; 26 | 27 | if (typeof window != "undefined") { 28 | // run in browser skip in next server 29 | fetch("/masks.json") 30 | .then((res) => res.json()) 31 | .catch((error) => { 32 | console.error("[Fetch] failed to fetch masks", error); 33 | return { cn: [], tw: [], en: [] }; 34 | }) 35 | .then((masks) => { 36 | const { cn = [], tw = [], en = [] } = masks; 37 | return [...cn, ...tw, ...en].map((m) => { 38 | BUILTIN_MASKS.push(BUILTIN_MASK_STORE.add(m)); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /app/icons/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/mj/mj.module.scss: -------------------------------------------------------------------------------- 1 | .mj-img-list{ 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: space-between; 5 | .mj-img-item-box{ 6 | width: 48%; 7 | &:not(:last-child){ 8 | margin-bottom: 20px; 9 | } 10 | } 11 | .mj-img-item{ 12 | .mj-img-item-info{ 13 | flex:1; 14 | width: 100%; 15 | overflow: hidden; 16 | user-select: text; 17 | p{ 18 | margin: 6px; 19 | font-size: 12px; 20 | } 21 | .line-1{ 22 | overflow: hidden; 23 | white-space: nowrap; 24 | text-overflow: ellipsis; 25 | } 26 | } 27 | .pre-img{ 28 | display: flex; 29 | width: 130px; 30 | justify-content: center; 31 | align-items: center; 32 | background-color: var(--second); 33 | border-radius: 10px; 34 | } 35 | .pre-img-loading{ 36 | flex-direction: column; 37 | font-size: 12px; 38 | } 39 | .img{ 40 | width: 130px; 41 | height: 130px; 42 | border-radius: 10px; 43 | overflow: hidden; 44 | cursor: pointer; 45 | transition: all .3s; 46 | &:hover{ 47 | opacity: .7; 48 | } 49 | } 50 | } 51 | } 52 | 53 | @media only screen and (max-width: 600px) { 54 | .mj-img-list{ 55 | .mj-img-item-box{ 56 | width: 100%; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/utils/cloudflare.ts: -------------------------------------------------------------------------------- 1 | export function cloudflareAIGatewayUrl(fetchUrl: string) { 2 | // rebuild fetchUrl, if using cloudflare ai gateway 3 | // document: https://developers.cloudflare.com/ai-gateway/providers/openai/ 4 | 5 | const paths = fetchUrl.split("/"); 6 | if ("gateway.ai.cloudflare.com" == paths[2]) { 7 | // is cloudflare.com ai gateway 8 | // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/azure-openai/{resource_name}/{deployment_name}/chat/completions?api-version=2023-05-15' 9 | if ("azure-openai" == paths[6]) { 10 | // is azure gateway 11 | return paths.slice(0, 8).concat(paths.slice(-3)).join("/"); // rebuild ai gateway azure_url 12 | } 13 | // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai/chat/completions 14 | if ("openai" == paths[6]) { 15 | // is openai gateway 16 | return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway openai_url 17 | } 18 | // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic/v1/messages \ 19 | if ("anthropic" == paths[6]) { 20 | // is anthropic gateway 21 | return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway anthropic_url 22 | } 23 | // TODO: Amazon Bedrock, Groq, HuggingFace... 24 | } 25 | return fetchUrl; 26 | } 27 | -------------------------------------------------------------------------------- /docs/vercel-cn.md: -------------------------------------------------------------------------------- 1 | # Vercel 的使用说明 2 | 3 | ## 如何新建项目 4 | 当你从 Github fork 本项目之后,需要重新在 Vercel 创建一个全新的 Vercel 项目来重新部署,你需要按照下列步骤进行。 5 | 6 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 7 | 1. 进入 Vercel 控制台首页; 8 | 2. 点击 Add New; 9 | 3. 选择 Project。 10 | 11 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 12 | 1. 在 Import Git Repository 处,搜索 chatgpt-next-web; 13 | 2. 选中新 fork 的项目,点击 Import。 14 | 15 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 16 | 1. 在项目配置页,点开 Environmane Variables 开始配置环境变量; 17 | 2. 依次新增名为 OPENAI_API_KEY 和 CODE ([访问密码](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/357296986609c14de10bf210871d30e2f67a8784/docs/faq-cn.md#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F-code-%E6%98%AF%E4%BB%80%E4%B9%88%E5%BF%85%E9%A1%BB%E8%AE%BE%E7%BD%AE%E5%90%97)) 的环境变量; 18 | 3. 填入环境变量对应的值; 19 | 4. 点击 Add 确认增加环境变量; 20 | 5. 请确保你添加了 OPENAI_API_KEY,否则无法使用; 21 | 6. 点击 Deploy,创建完成,耐心等待 5 分钟左右部署完成。 22 | 23 | ## 如何增加自定义域名 24 | [TODO] 25 | 26 | ## 如何更改环境变量 27 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 28 | 1. 进去 Vercel 项目内部控制台,点击顶部的 Settings 按钮; 29 | 2. 点击左侧的 Environment Variables; 30 | 3. 点击已有条目的右侧按钮; 31 | 4. 选择 Edit 进行编辑,然后保存即可。 32 | 33 | ⚠️️ 注意:每次修改完环境变量,你都需要[重新部署项目](#如何重新部署)来让改动生效! 34 | 35 | ## 如何重新部署 36 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 37 | 1. 进入 Vercel 项目内部控制台,点击顶部的 Deployments 按钮; 38 | 2. 选择列表最顶部一条的右侧按钮; 39 | 3. 点击 Redeploy 即可重新部署。 40 | -------------------------------------------------------------------------------- /docs/vercel-ja.md: -------------------------------------------------------------------------------- 1 | # Vercel 使用説明書 2 | 3 | ## 新規プロジェクトの作成方法 4 | 5 | このプロジェクトを GitHub からフォークし、Vercel で新しい Vercel プロジェクトを作成して再デプロイする必要がある場合は、以下の手順に従ってください。 6 | 7 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 8 | 9 | 1. Vercel コンソールのホームページにアクセスします; 10 | 2. 新規追加をクリックする; 11 | 3. プロジェクトを選択します。 12 | 13 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 14 | 15 | 1. Git リポジトリのインポートで、chatgpt-next-web を検索します; 16 | 2 .新しいフォークプロジェクトを選択し、インポートをクリックします。 17 | 18 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 19 | 20 | 1. Project Settings ページで、Environment Variables をクリックして環境変数を設定する; 21 | 2. OPENAI_API_KEY と CODE という名前の環境変数を追加します; 22 | 3. 環境変数に対応する値を入力します; 23 | 4. Add をクリックして、環境変数の追加を確認する; 24 | 5. OPENAI_API_KEY を必ず追加してください; 25 | 6. Deploy をクリックして作成し、デプロイが完了するまで約 5 分間辛抱強く待つ。 26 | 27 | ## カスタムドメイン名の追加方法 28 | 29 | \[TODO] 30 | 31 | ## 環境変数の変更方法 32 | 33 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 34 | 35 | 1. 内部 Vercel プロジェクトコンソールに移動し、上部の設定ボタンをクリックします; 36 | 2. 左側の Environment Variables をクリックします; 37 | 3. 既存のエントリーの右側のボタンをクリックします; 38 | 4. 編集を選択して編集し、保存する。 39 | 40 | ⚠️️ 注意: [プロジェクトの再デプロイ](#再実装の方法)環境変数を変更するたびに、変更を有効にするために必要です! 41 | 42 | ## 再実装の方法 43 | 44 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 45 | 46 | 1. Vercelプロジェクトの内部コンソールに移動し、一番上のDeploymentsボタンをクリックします; 47 | 2. リストの一番上の項目の右のボタンを選択します; 48 | 3. 再デプロイをクリックして再デプロイします。 49 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-ko.md: -------------------------------------------------------------------------------- 1 | ## Cloudflare 페이지 배포 가이드 2 | 3 | ## 새 프로젝트를 만드는 방법 4 | 이 프로젝트를 Github에서 포크한 다음 dash.cloudflare.com에 로그인하고 페이지로 이동합니다. 5 | 6 | 1. "프로젝트 만들기"를 클릭합니다. 7 | 2. "Git에 연결"을 선택합니다. 8 | 3. Cloudflare 페이지를 GitHub 계정과 연결합니다. 9 | 4. 포크한 프로젝트를 선택합니다. 10 | 5. "설정 시작"을 클릭합니다. 11 | 6. "프로젝트 이름" 및 "프로덕션 브랜치"의 기본값을 사용하거나 필요에 따라 변경합니다. 12 | 7. "빌드 설정"에서 "프레임워크 프리셋" 옵션을 선택하고 "Next.js"를 선택합니다. 13 | 8. node:buffer 버그로 인해 지금은 기본 "빌드 명령어"를 사용하지 마세요. 다음 명령을 사용하세요: 14 | ``` 15 | npx @cloudflare/next-on-pages --experimental-minify 16 | ``` 17 | 9. "빌드 출력 디렉토리"의 경우 기본값을 사용하고 수정하지 마십시오. 18 | 10. "루트 디렉토리"는 수정하지 마십시오. 19 | 11. "환경 변수"의 경우 ">"를 클릭한 다음 "변수 추가"를 클릭합니다. 다음에 따라 정보를 입력합니다: 20 | 21 | - node_version=20.1`. 22 | - next_telemetry_disable=1`. 23 | - `OPENAI_API_KEY=자신의 API 키` 24 | - ``yarn_version=1.22.19`` 25 | - ``php_version=7.4``. 26 | 27 | 실제 필요에 따라 다음 옵션을 선택적으로 입력합니다: 28 | 29 | - `CODE= 선택적으로 액세스 비밀번호를 입력하며 쉼표를 사용하여 여러 비밀번호를 구분할 수 있습니다`. 30 | - `OPENAI_ORG_ID= 선택 사항, OpenAI에서 조직 ID 지정` 31 | - `HIDE_USER_API_KEY=1 선택 사항, 사용자가 API 키를 입력하지 못하도록 합니다. 32 | - `DISABLE_GPT4=1 옵션, 사용자가 GPT-4를 사용하지 못하도록 설정` 12. 33 | 34 | 12. "저장 후 배포"를 클릭합니다. 35 | 13. 호환성 플래그를 입력해야 하므로 "배포 취소"를 클릭합니다. 36 | 14. "빌드 설정", "기능"으로 이동하여 "호환성 플래그"를 찾습니다. 37 | "프로덕션 호환성 플래그 구성" 및 "프리뷰 호환성 플래그 구성"에서 "nodejs_compat"를 입력합니다. 38 | 16. "배포"로 이동하여 "배포 다시 시도"를 클릭합니다. 39 | 17. 즐기세요! -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | chatgpt-next-web: 4 | profiles: [ "no-proxy" ] 5 | container_name: chatany 6 | image: licoy/chatany 7 | ports: 8 | - 3000:3000 9 | environment: 10 | - OPENAI_API_KEY=$OPENAI_API_KEY 11 | - GOOGLE_API_KEY=$GOOGLE_API_KEY 12 | - CODE=$CODE 13 | - BASE_URL=$BASE_URL 14 | - OPENAI_ORG_ID=$OPENAI_ORG_ID 15 | - HIDE_USER_API_KEY=$HIDE_USER_API_KEY 16 | - DISABLE_GPT4=$DISABLE_GPT4 17 | - ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY 18 | - DISABLE_FAST_LINK=$DISABLE_FAST_LINK 19 | - OPENAI_SB=$OPENAI_SB 20 | - MJ_PROXY_URL=$MJ_PROXY_URL 21 | - MJ_PROXY_KEY=$MJ_PROXY_KEY 22 | 23 | chatgpt-next-web-proxy: 24 | profiles: [ "proxy" ] 25 | container_name: chatany 26 | image: licoy/chatany 27 | ports: 28 | - 3000:3000 29 | environment: 30 | - OPENAI_API_KEY=$OPENAI_API_KEY 31 | - GOOGLE_API_KEY=$GOOGLE_API_KEY 32 | - CODE=$CODE 33 | - PROXY_URL=$PROXY_URL 34 | - BASE_URL=$BASE_URL 35 | - OPENAI_ORG_ID=$OPENAI_ORG_ID 36 | - HIDE_USER_API_KEY=$HIDE_USER_API_KEY 37 | - DISABLE_GPT4=$DISABLE_GPT4 38 | - ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY 39 | - DISABLE_FAST_LINK=$DISABLE_FAST_LINK 40 | - OPENAI_SB=$OPENAI_SB 41 | - MJ_PROXY_URL=$MJ_PROXY_URL 42 | - MJ_PROXY_KEY=$MJ_PROXY_KEY 43 | -------------------------------------------------------------------------------- /app/api/azure/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import { getServerSideConfig } from "@/app/config/server"; 2 | import { ModelProvider } from "@/app/constant"; 3 | import { prettyObject } from "@/app/utils/format"; 4 | import { NextRequest, NextResponse } from "next/server"; 5 | import { auth } from "../../auth"; 6 | import { requestOpenai } from "../../common"; 7 | 8 | async function handle( 9 | req: NextRequest, 10 | { params }: { params: { path: string[] } }, 11 | ) { 12 | console.log("[Azure Route] params ", params); 13 | 14 | if (req.method === "OPTIONS") { 15 | return NextResponse.json({ body: "OK" }, { status: 200 }); 16 | } 17 | 18 | const subpath = params.path.join("/"); 19 | 20 | const authResult = auth(req, ModelProvider.GPT); 21 | if (authResult.error) { 22 | return NextResponse.json(authResult, { 23 | status: 401, 24 | }); 25 | } 26 | 27 | try { 28 | return await requestOpenai(req); 29 | } catch (e) { 30 | console.error("[Azure] ", e); 31 | return NextResponse.json(prettyObject(e)); 32 | } 33 | } 34 | 35 | export const GET = handle; 36 | export const POST = handle; 37 | 38 | export const runtime = "edge"; 39 | export const preferredRegion = [ 40 | "arn1", 41 | "bom1", 42 | "cdg1", 43 | "cle1", 44 | "cpt1", 45 | "dub1", 46 | "fra1", 47 | "gru1", 48 | "hnd1", 49 | "iad1", 50 | "icn1", 51 | "kix1", 52 | "lhr1", 53 | "pdx1", 54 | "sfo1", 55 | "sin1", 56 | "syd1", 57 | ]; 58 | -------------------------------------------------------------------------------- /app/config/build.ts: -------------------------------------------------------------------------------- 1 | import tauriConfig from "../../src-tauri/tauri.conf.json"; 2 | import { DEFAULT_INPUT_TEMPLATE } from "../constant"; 3 | 4 | export const getBuildConfig = () => { 5 | if (typeof process === "undefined") { 6 | throw Error( 7 | "[Server Config] you are importing a nodejs-only module outside of nodejs", 8 | ); 9 | } 10 | 11 | const buildMode = process.env.BUILD_MODE ?? "standalone"; 12 | const isApp = !!process.env.BUILD_APP; 13 | const version = "v" + tauriConfig.package.version; 14 | 15 | const commitInfo = (() => { 16 | try { 17 | const childProcess = require("child_process"); 18 | const commitDate: string = childProcess 19 | .execSync('git log -1 --format="%at000" --date=unix') 20 | .toString() 21 | .trim(); 22 | const commitHash: string = childProcess 23 | .execSync('git log --pretty=format:"%H" -n 1') 24 | .toString() 25 | .trim(); 26 | 27 | return { commitDate, commitHash }; 28 | } catch (e) { 29 | console.error("[Build Config] No git or not from git repo."); 30 | return { 31 | commitDate: "unknown", 32 | commitHash: "unknown", 33 | }; 34 | } 35 | })(); 36 | 37 | return { 38 | version, 39 | ...commitInfo, 40 | buildMode, 41 | isApp, 42 | template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE, 43 | }; 44 | }; 45 | 46 | export type BuildConfig = ReturnType; 47 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | push_to_registry: 10 | name: Push Docker image to Docker Hub 11 | runs-on: ubuntu-latest 12 | steps: 13 | - 14 | name: Check out the repo 15 | uses: actions/checkout@v3 16 | - 17 | name: Log in to Docker Hub 18 | uses: docker/login-action@v2 19 | with: 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | 23 | - 24 | name: Extract metadata (tags, labels) for Docker 25 | id: meta 26 | uses: docker/metadata-action@v4 27 | with: 28 | images: licoy/chatany 29 | tags: | 30 | type=raw,value=latest 31 | type=ref,event=tag 32 | 33 | - 34 | name: Set up QEMU 35 | uses: docker/setup-qemu-action@v2 36 | 37 | - 38 | name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v2 40 | 41 | - 42 | name: Build and push Docker image 43 | uses: docker/build-push-action@v4 44 | with: 45 | context: . 46 | platforms: linux/amd64,linux/arm64 47 | push: true 48 | tags: ${{ steps.meta.outputs.tags }} 49 | labels: ${{ steps.meta.outputs.labels }} 50 | cache-from: type=gha 51 | cache-to: type=gha,mode=max 52 | 53 | -------------------------------------------------------------------------------- /docs/vercel-ko.md: -------------------------------------------------------------------------------- 1 | # Vercel 사용 방법 2 | 3 | ## 새 프로젝트 생성 방법 4 | 이 프로젝트를 Github에서 포크한 후, 다시 배포하려면 Vercel에서 새로운 Vercel 프로젝트를 생성해야 하며, 다음 단계를 따라야 합니다. 5 | 6 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 7 | 1. Vercel 콘솔 홈 페이지로 이동합니다; 8 | 2. 새로 추가를 클릭합니다; 9 | 3. 프로젝트를 선택합니다. 10 | 11 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 12 | 1. Git 리포지토리 가져오기에서 chatgpt-next-web을 검색합니다. 2. 새 포크를 선택합니다; 13 | 2. 새로 포크된 프로젝트를 선택하고 가져오기를 클릭합니다. 14 | 15 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 16 | 1. 프로젝트 구성 페이지에서 환경 변수 설정을 클릭하여 환경 변수 설정을 시작합니다; 17 | 2. OPENAI_API_KEY, CODE ([Access Code](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/357296986609c14de10bf210871d30e2f67a8784/docs/faq-cn.md#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F-code-%E6%98%AF%E4%BB%80%E4%B9%88%E5%BF%85%E9%A1%BB%E8%AE%BE%E7%BD%AE%E5%90%97)). 환경 변수를 설정합니다; 18 | 3. 환경 변수의 값을 입력합니다; 19 | 4. 추가를 클릭하여 환경 변수 추가를 확인합니다; 20 | 5. OPENAI_API_KEY를 추가해야 하며, 그렇지 않으면 작동하지 않습니다; 21 | 6. 배포를 클릭하여 도메인 이름 생성을 완료하고 배포가 완료될 때까지 약 5분간 기다립니다. 22 | 23 | ## 사용자 정의 도메인 네임 추가 방법 24 | [TODO] 25 | 26 | ## 환경 변수 변경 방법 27 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 28 | 1. 버셀 프로젝트의 내부 콘솔로 이동하여 상단의 설정 버튼을 클릭합니다; 29 | 2. 왼쪽의 환경 변수를 클릭합니다; 30 | 3. 기존 항목 오른쪽에 있는 버튼을 클릭합니다; 31 | 4. 편집을 선택하여 수정하고 저장합니다. 32 | 33 | ⚠️️ 참고: 환경 변수를 변경할 때마다 [프로젝트를 재배포](#如何重新部署)해야 변경 사항을 적용할 수 있습니다! 34 | 35 | ## 재배포 방법 36 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 37 | 1. 버셀 내부 프로젝트 콘솔로 이동하여 상단의 배포 버튼을 클릭합니다; 38 | 2. 목록에서 맨 위 항목 오른쪽에 있는 버튼을 선택합니다; 39 | 3. 재배포를 클릭하여 재배포합니다. -------------------------------------------------------------------------------- /app/components/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import styles from "./button.module.scss"; 4 | import { CSSProperties } from "react"; 5 | 6 | export type ButtonType = "primary" | "danger" | "success" | "purple" | null; 7 | 8 | export function IconButton(props: { 9 | onClick?: () => void; 10 | icon?: JSX.Element; 11 | type?: ButtonType; 12 | text?: string; 13 | bordered?: boolean; 14 | shadow?: boolean; 15 | className?: string; 16 | title?: string; 17 | disabled?: boolean; 18 | tabIndex?: number; 19 | autoFocus?: boolean; 20 | style?: CSSProperties; 21 | }) { 22 | return ( 23 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /app/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nextchat" 3 | version = "0.1.0" 4 | description = "A cross platform app for LLM ChatBot." 5 | authors = ["Yidadaa"] 6 | license = "mit" 7 | repository = "" 8 | default-run = "nextchat" 9 | edition = "2021" 10 | rust-version = "1.60" 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.5.1", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.5.4", features = [ 21 | "notification-all", 22 | "fs-all", 23 | "clipboard-all", 24 | "dialog-all", 25 | "shell-open", 26 | "updater", 27 | "window-close", 28 | "window-hide", 29 | "window-maximize", 30 | "window-minimize", 31 | "window-set-icon", 32 | "window-set-ignore-cursor-events", 33 | "window-set-resizable", 34 | "window-show", 35 | "window-start-dragging", 36 | "window-unmaximize", 37 | "window-unminimize", 38 | ] } 39 | tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } 40 | 41 | [features] 42 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. 43 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. 44 | # DO NOT REMOVE!! 45 | custom-protocol = ["tauri/custom-protocol"] 46 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-cn.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages 部署指南 2 | 3 | ## 如何新建项目 4 | 5 | 在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。 6 | 7 | 1. 点击 "Create a project"。 8 | 2. 选择 "Connect to Git"。 9 | 3. 关联 Cloudflare Pages 和你的 GitHub 账号。 10 | 4. 选中你 fork 的此项目。 11 | 5. 点击 "Begin setup"。 12 | 6. 对于 "Project name" 和 "Production branch",可以使用默认值,也可以根据需要进行更改。 13 | 7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。 14 | 8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令: 15 | ``` 16 | npx @cloudflare/next-on-pages --experimental-minify 17 | ``` 18 | 9. 对于 "Build output directory",使用默认值并且不要修改。 19 | 10. 不要修改 "Root Directory"。 20 | 11. 对于 "Environment variables",点击 ">" 然后点击 "Add variable"。按照以下信息填写: 21 | 22 | - `NODE_VERSION=20.1` 23 | - `NEXT_TELEMETRY_DISABLE=1` 24 | - `OPENAI_API_KEY=你自己的API Key` 25 | - `YARN_VERSION=1.22.19` 26 | - `PHP_VERSION=7.4` 27 | 28 | 根据实际需要,可以选择填写以下选项: 29 | 30 | - `CODE= 可选填,访问密码,可以使用逗号隔开多个密码` 31 | - `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID` 32 | - `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key` 33 | - `DISABLE_GPT4=1 可选,不让用户使用 GPT-4` 34 | - `ENABLE_BALANCE_QUERY=1 可选,启用余额查询功能` 35 | - `DISABLE_FAST_LINK=1 可选,禁用从链接解析预制设置` 36 | 37 | 12. 点击 "Save and Deploy"。 38 | 13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。 39 | 14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。 40 | 15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。 41 | 16. 前往 "Deployments",点击 "Retry deployment"。 42 | 17. Enjoy. 43 | -------------------------------------------------------------------------------- /scripts/delete-deployment-preview.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Set the pipefail option. 3 | set -o pipefail 4 | 5 | # Get the Vercel API endpoints. 6 | GET_DEPLOYMENTS_ENDPOINT="https://api.vercel.com/v6/deployments" 7 | DELETE_DEPLOYMENTS_ENDPOINT="https://api.vercel.com/v13/deployments" 8 | 9 | # Create a list of deployments. 10 | deployments=$(curl -s -X GET "$GET_DEPLOYMENTS_ENDPOINT/?projectId=$VERCEL_PROJECT_ID&teamId=$VERCEL_ORG_ID" -H "Authorization: Bearer $VERCEL_TOKEN ") 11 | #deployments=$(curl -s -X GET "$GET_DEPLOYMENTS_ENDPOINT/?projectId=$VERCEL_PROJECT_ID" -H "Authorization: Bearer $VERCEL_TOKEN ") 12 | 13 | # Filter the deployments list by meta.base_hash === meta tag. 14 | filtered_deployments=$(echo -E $deployments | jq --arg META_TAG "$META_TAG" '[.deployments[] | select(.meta.base_hash | type == "string" and contains($META_TAG)) | .uid] | join(",")') 15 | filtered_deployments="${filtered_deployments//\"/}" # Remove double quotes 16 | 17 | # Clears the values from filtered_deployments 18 | IFS=',' read -ra values <<<"$filtered_deployments" 19 | 20 | echo "META_TAG ${META_TAG}" 21 | echo "Filtered deployments ${filtered_deployments}" 22 | 23 | # Iterate over the filtered deployments list. 24 | for uid in "${values[@]}"; do 25 | echo "Deleting ${uid}" 26 | 27 | delete_url="${DELETE_DEPLOYMENTS_ENDPOINT}/${uid}?teamId=${VERCEL_ORG_ID}" 28 | echo $delete_url 29 | 30 | # Make DELETE a request to the /v13/deployments/{id} endpoint. 31 | curl -X DELETE $delete_url -H "Authorization: Bearer $VERCEL_TOKEN" 32 | 33 | echo "Deleted!" 34 | done 35 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | *.lcov 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Node.js dependencies 28 | /node_modules 29 | /jspm_packages 30 | 31 | # TypeScript v1 declaration files 32 | typings 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Output of 'npm pack' 44 | *.tgz 45 | 46 | # Yarn Integrity file 47 | .yarn-integrity 48 | 49 | # dotenv environment variable files 50 | .env 51 | .env.test 52 | 53 | # local env files 54 | .env*.local 55 | 56 | # Next.js build output 57 | .next 58 | out 59 | 60 | # Nuxt.js build output 61 | .nuxt 62 | dist 63 | 64 | # Gatsby files 65 | .cache/ 66 | 67 | 68 | # Vuepress build output 69 | .vuepress/dist 70 | 71 | # Serverless directories 72 | .serverless/ 73 | 74 | # FuseBox cache 75 | .fusebox/ 76 | 77 | # DynamoDB Local files 78 | .dynamodb/ 79 | 80 | # Temporary folders 81 | tmp 82 | temp 83 | 84 | # IDE and editor directories 85 | .idea 86 | .vscode 87 | *.swp 88 | *.swo 89 | *~ 90 | 91 | # OS generated files 92 | .DS_Store 93 | Thumbs.db 94 | 95 | # secret key 96 | *.key 97 | *.key.pub 98 | -------------------------------------------------------------------------------- /app/components/settings.module.scss: -------------------------------------------------------------------------------- 1 | .settings { 2 | padding: 20px; 3 | overflow: auto; 4 | } 5 | 6 | .avatar { 7 | cursor: pointer; 8 | position: relative; 9 | z-index: 1; 10 | } 11 | 12 | .edit-prompt-modal { 13 | display: flex; 14 | flex-direction: column; 15 | 16 | .edit-prompt-title { 17 | max-width: unset; 18 | margin-bottom: 20px; 19 | text-align: left; 20 | } 21 | .edit-prompt-content { 22 | max-width: unset; 23 | } 24 | } 25 | 26 | .user-prompt-modal { 27 | min-height: 40vh; 28 | 29 | .user-prompt-search { 30 | width: 100%; 31 | max-width: 100%; 32 | margin-bottom: 10px; 33 | background-color: var(--gray); 34 | } 35 | 36 | .user-prompt-list { 37 | border: var(--border-in-light); 38 | border-radius: 10px; 39 | 40 | .user-prompt-item { 41 | display: flex; 42 | justify-content: space-between; 43 | padding: 10px; 44 | 45 | &:not(:last-child) { 46 | border-bottom: var(--border-in-light); 47 | } 48 | 49 | .user-prompt-header { 50 | max-width: calc(100% - 100px); 51 | 52 | .user-prompt-title { 53 | font-size: 14px; 54 | line-height: 2; 55 | font-weight: bold; 56 | } 57 | .user-prompt-content { 58 | font-size: 12px; 59 | } 60 | } 61 | 62 | .user-prompt-buttons { 63 | display: flex; 64 | align-items: center; 65 | column-gap: 2px; 66 | 67 | .user-prompt-button { 68 | //height: 100%; 69 | padding: 7px; 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/brain.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-ja.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages 導入ガイド 2 | 3 | ## 新規プロジェクトの作成方法 4 | GitHub でこのプロジェクトをフォークし、dash.cloudflare.com にログインして Pages にアクセスします。 5 | 6 | 1. "Create a project" をクリックする。 7 | 2. "Connect to Git" を選択する。 8 | 3. Cloudflare Pages を GitHub アカウントに接続します。 9 | 4. フォークしたプロジェクトを選択します。 10 | 5. "Begin setup" をクリックする。 11 | 6. "Project name" と "Production branch" はデフォルト値を使用するか、必要に応じて変更してください。 12 | 7. "Build Settings" で、"Framework presets" オプションを選択し、"Next.js" を選択します。 13 | 8. node:buffer のバグのため、デフォルトの "Build command" は使用しないでください。代わりに、以下のコマンドを使用してください: 14 | ``` 15 | npx @cloudflare/next-on-pages --experimental-minify 16 | ``` 17 | 9. "Build output directory" はデフォルト値を使用し、変更しない。 18 | 10. "Root Directory" を変更しない。 19 | 11. "Environment variables" は、">" をクリックし、"Add variable" をクリックします。そして以下の情報を入力します: 20 | - `NODE_VERSION=20.1` 21 | - `NEXT_TELEMETRY_DISABLE=1` 22 | - `OPENAI_API_KEY=your_own_API_key` 23 | - `YARN_VERSION=1.22.19` 24 | - `PHP_VERSION=7.4` 25 | 26 | 必要に応じて、以下の項目を入力してください: 27 | 28 | - `CODE= Optional, access passwords, multiple passwords can be separated by commas` 29 | - `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI` 30 | - `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key` 31 | - `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4` 32 | 33 | 12. "Save and Deploy" をクリックする。 34 | 13. 互換性フラグを記入する必要があるため、"Cancel deployment" をクリックする。 35 | 14. "Build settings" の "Functions" から "Compatibility flags" を見つける。 36 | 15. "Configure Production compatibility flag" と "Configure Preview compatibility flag" の両方に "nodejs_compat "を記入する。 37 | 16. "Deployments" に移動し、"Retry deployment" をクリックします。 38 | 17. お楽しみください。 39 | -------------------------------------------------------------------------------- /app/components/message-selector.module.scss: -------------------------------------------------------------------------------- 1 | .message-selector { 2 | .message-filter { 3 | display: flex; 4 | 5 | .search-bar { 6 | max-width: unset; 7 | flex-grow: 1; 8 | margin-right: 10px; 9 | } 10 | 11 | .actions { 12 | display: flex; 13 | 14 | button:not(:last-child) { 15 | margin-right: 10px; 16 | } 17 | } 18 | 19 | @media screen and (max-width: 600px) { 20 | flex-direction: column; 21 | 22 | .search-bar { 23 | margin-right: 0; 24 | } 25 | 26 | .actions { 27 | margin-top: 20px; 28 | 29 | button { 30 | flex-grow: 1; 31 | } 32 | } 33 | } 34 | } 35 | 36 | .messages { 37 | margin-top: 20px; 38 | border-radius: 10px; 39 | border: var(--border-in-light); 40 | overflow: hidden; 41 | 42 | .message { 43 | display: flex; 44 | align-items: center; 45 | padding: 8px 10px; 46 | cursor: pointer; 47 | 48 | &-selected { 49 | background-color: var(--second); 50 | } 51 | 52 | &:not(:last-child) { 53 | border-bottom: var(--border-in-light); 54 | } 55 | 56 | .avatar { 57 | margin-right: 10px; 58 | } 59 | 60 | .body { 61 | flex: 1; 62 | max-width: calc(100% - 80px); 63 | 64 | .date { 65 | font-size: 12px; 66 | line-height: 1.2; 67 | opacity: 0.5; 68 | } 69 | 70 | .content { 71 | font-size: 12px; 72 | } 73 | } 74 | 75 | .checkbox { 76 | display: flex; 77 | justify-content: flex-end; 78 | flex: 1; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/icons/eye-off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/utils/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { combine, persist } from "zustand/middleware"; 3 | import { Updater } from "../typing"; 4 | import { deepClone } from "./clone"; 5 | 6 | type SecondParam = T extends ( 7 | _f: infer _F, 8 | _s: infer S, 9 | ...args: infer _U 10 | ) => any 11 | ? S 12 | : never; 13 | 14 | type MakeUpdater = { 15 | lastUpdateTime: number; 16 | 17 | markUpdate: () => void; 18 | update: Updater; 19 | }; 20 | 21 | type SetStoreState = ( 22 | partial: T | Partial | ((state: T) => T | Partial), 23 | replace?: boolean | undefined, 24 | ) => void; 25 | 26 | export function createPersistStore( 27 | state: T, 28 | methods: ( 29 | set: SetStoreState>, 30 | get: () => T & MakeUpdater, 31 | ) => M, 32 | persistOptions: SecondParam>>, 33 | ) { 34 | return create( 35 | persist( 36 | combine( 37 | { 38 | ...state, 39 | lastUpdateTime: 0, 40 | }, 41 | (set, get) => { 42 | return { 43 | ...methods(set, get as any), 44 | 45 | markUpdate() { 46 | set({ lastUpdateTime: Date.now() } as Partial< 47 | T & M & MakeUpdater 48 | >); 49 | }, 50 | update(updater) { 51 | const state = deepClone(get()); 52 | updater(state); 53 | set({ 54 | ...state, 55 | lastUpdateTime: Date.now(), 56 | }); 57 | }, 58 | } as M & MakeUpdater; 59 | }, 60 | ), 61 | persistOptions as any, 62 | ), 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /app/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS base 2 | 3 | FROM base AS deps 4 | 5 | RUN apk add --no-cache libc6-compat 6 | 7 | WORKDIR /app 8 | 9 | COPY package.json yarn.lock ./ 10 | 11 | RUN yarn config set registry 'https://registry.npmmirror.com/' 12 | RUN yarn install 13 | 14 | FROM base AS builder 15 | 16 | RUN apk update && apk add --no-cache git 17 | 18 | ENV OPENAI_API_KEY="" 19 | ENV GOOGLE_API_KEY="" 20 | ENV CODE="" 21 | 22 | WORKDIR /app 23 | COPY --from=deps /app/node_modules ./node_modules 24 | COPY . . 25 | 26 | RUN yarn build 27 | 28 | FROM base AS runner 29 | WORKDIR /app 30 | 31 | RUN apk add proxychains-ng 32 | 33 | ENV PROXY_URL="" 34 | ENV OPENAI_API_KEY="" 35 | ENV GOOGLE_API_KEY="" 36 | ENV CODE="" 37 | 38 | COPY --from=builder /app/public ./public 39 | COPY --from=builder /app/.next/standalone ./ 40 | COPY --from=builder /app/.next/static ./.next/static 41 | COPY --from=builder /app/.next/server ./.next/server 42 | 43 | EXPOSE 3000 44 | 45 | CMD if [ -n "$PROXY_URL" ]; then \ 46 | export HOSTNAME="0.0.0.0"; \ 47 | protocol=$(echo $PROXY_URL | cut -d: -f1); \ 48 | host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ 49 | port=$(echo $PROXY_URL | cut -d: -f3); \ 50 | conf=/etc/proxychains.conf; \ 51 | echo "strict_chain" > $conf; \ 52 | echo "proxy_dns" >> $conf; \ 53 | echo "remote_dns_subnet 224" >> $conf; \ 54 | echo "tcp_read_time_out 15000" >> $conf; \ 55 | echo "tcp_connect_time_out 8000" >> $conf; \ 56 | echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \ 57 | echo "localnet ::1/128" >> $conf; \ 58 | echo "[ProxyList]" >> $conf; \ 59 | echo "$protocol $host $port" >> $conf; \ 60 | cat /etc/proxychains.conf; \ 61 | proxychains -f $conf node server.js; \ 62 | else \ 63 | node server.js; \ 64 | fi 65 | -------------------------------------------------------------------------------- /app/components/emoji.tsx: -------------------------------------------------------------------------------- 1 | import EmojiPicker, { 2 | Emoji, 3 | EmojiStyle, 4 | Theme as EmojiTheme, 5 | } from "emoji-picker-react"; 6 | 7 | import { ModelType } from "../store"; 8 | 9 | import BotIcon from "../icons/bot.svg"; 10 | import BlackBotIcon from "../icons/black-bot.svg"; 11 | 12 | export function getEmojiUrl(unified: string, style: EmojiStyle) { 13 | // Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis 14 | // Old CDN broken, so I had to switch to this one 15 | // Author: https://github.com/H0llyW00dzZ 16 | return `https://fastly.jsdelivr.net/npm/emoji-datasource-apple/img/${style}/64/${unified}.png`; 17 | } 18 | 19 | export function AvatarPicker(props: { 20 | onEmojiClick: (emojiId: string) => void; 21 | }) { 22 | return ( 23 | { 29 | props.onEmojiClick(e.unified); 30 | }} 31 | /> 32 | ); 33 | } 34 | 35 | export function Avatar(props: { model?: ModelType; avatar?: string }) { 36 | if (props.model) { 37 | return ( 38 |
39 | {props.model?.startsWith("gpt-4") ? ( 40 | 41 | ) : ( 42 | 43 | )} 44 |
45 | ); 46 | } 47 | 48 | return ( 49 |
50 | {props.avatar && } 51 |
52 | ); 53 | } 54 | 55 | export function EmojiAvatar(props: { avatar: string; size?: number }) { 56 | return ( 57 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /app/icons/max.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-page-custom-font */ 2 | import "./styles/globals.scss"; 3 | import "./styles/markdown.scss"; 4 | import "./styles/highlight.scss"; 5 | import { getClientConfig } from "./config/client"; 6 | import type { Metadata, Viewport } from "next"; 7 | import { SpeedInsights } from "@vercel/speed-insights/next"; 8 | import { getServerSideConfig } from "./config/server"; 9 | import { GoogleTagManager } from "@next/third-parties/google"; 10 | const serverConfig = getServerSideConfig(); 11 | 12 | export const metadata: Metadata = { 13 | title: "ChatAny", 14 | description: "Your personal ChatGPT Chat Bot.", 15 | appleWebApp: { 16 | title: "ChatAny", 17 | statusBarStyle: "default", 18 | }, 19 | }; 20 | 21 | export const viewport: Viewport = { 22 | width: "device-width", 23 | initialScale: 1, 24 | maximumScale: 1, 25 | themeColor: [ 26 | { media: "(prefers-color-scheme: light)", color: "#fafafa" }, 27 | { media: "(prefers-color-scheme: dark)", color: "#151515" }, 28 | ], 29 | }; 30 | 31 | export default function RootLayout({ 32 | children, 33 | }: { 34 | children: React.ReactNode; 35 | }) { 36 | return ( 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | {children} 49 | {serverConfig?.isVercel && ( 50 | <> 51 | 52 | 53 | )} 54 | {serverConfig?.gtmId && ( 55 | <> 56 | 57 | 58 | )} 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /app/components/mj/mj-panel.module.scss: -------------------------------------------------------------------------------- 1 | .ctrl-param-item { 2 | display: flex; 3 | justify-content: space-between; 4 | min-height: 40px; 5 | padding: 10px 0; 6 | animation: slide-in ease 0.6s; 7 | flex-direction: column; 8 | 9 | .ctrl-param-item-header { 10 | display: flex; 11 | align-items: center; 12 | 13 | .ctrl-param-item-title { 14 | font-size: 14px; 15 | font-weight: bolder; 16 | margin-bottom: 5px; 17 | } 18 | } 19 | 20 | .ctrl-param-item-sub-title { 21 | font-size: 12px; 22 | font-weight: normal; 23 | margin-top: 3px; 24 | } 25 | textarea { 26 | appearance: none; 27 | border-radius: 10px; 28 | border: var(--border-in-light); 29 | min-height: 36px; 30 | box-sizing: border-box; 31 | background: var(--white); 32 | color: var(--black); 33 | padding: 0 10px; 34 | max-width: 50%; 35 | font-family: inherit; 36 | } 37 | } 38 | 39 | .ai-models { 40 | display: flex; 41 | button { 42 | margin-bottom: 10px; 43 | padding: 10px; 44 | width: 100%; 45 | &:not(:last-child){ 46 | margin-right: 10px; 47 | } 48 | } 49 | } 50 | 51 | .image-upload-btn-list{ 52 | display: flex; 53 | justify-content: flex-start; 54 | align-items: center; 55 | flex-wrap: wrap; 56 | } 57 | 58 | .image-upload-btn{ 59 | width:100px; 60 | height:100px; 61 | border:rgba(0, 0, 0, 0.2) 1px solid; 62 | border-radius:10px; 63 | display:flex; 64 | justify-content:center; 65 | align-items:center; 66 | color:rgba(0, 0, 0, 0.2); 67 | transition: all .3s; 68 | cursor: pointer; 69 | margin-bottom: 10px; 70 | &:not(:last-child){ 71 | margin-right: 10px; 72 | } 73 | &:hover{ 74 | background-color:rgba(0, 0, 0, 0.1); 75 | } 76 | .upload-btn-entry{ 77 | padding:10px; 78 | display:flex; 79 | justify-content:center; 80 | align-items:center; 81 | } 82 | img{ 83 | width: 100%; 84 | height: 100%; 85 | border-radius:10px; 86 | } 87 | } -------------------------------------------------------------------------------- /app/icons/min.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/mask.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # Your openai api key. (required) 2 | OPENAI_API_KEY=sk-xxxx 3 | 4 | # Access password, separated by comma. (optional) 5 | CODE=your-password 6 | 7 | # You can start service behind a proxy. (optional) 8 | PROXY_URL=http://localhost:7890 9 | 10 | # (optional) 11 | # Default: Empty 12 | # Google Gemini Pro API key, set if you want to use Google Gemini Pro API. 13 | GOOGLE_API_KEY= 14 | 15 | # (optional) 16 | # Default: https://generativelanguage.googleapis.com/ 17 | # Google Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url. 18 | GOOGLE_URL= 19 | 20 | # Override openai api request base url. (optional) 21 | # Default: https://api.openai.com 22 | # Examples: http://your-openai-proxy.com 23 | BASE_URL= 24 | 25 | # Specify OpenAI organization ID.(optional) 26 | # Default: Empty 27 | OPENAI_ORG_ID= 28 | 29 | # (optional) 30 | # Default: Empty 31 | # If you do not want users to use GPT-4, set this value to 1. 32 | DISABLE_GPT4= 33 | 34 | # (optional) 35 | # Default: Empty 36 | # If you do not want users to input their own API key, set this value to 1. 37 | HIDE_USER_API_KEY= 38 | 39 | # (optional) 40 | # Default: Empty 41 | # If you do want users to query balance, set this value to 1. 42 | ENABLE_BALANCE_QUERY= 43 | 44 | # (optional) 45 | # Default: Empty 46 | # If you want to disable parse settings from url, set this value to 1. 47 | DISABLE_FAST_LINK= 48 | 49 | # (optional) 50 | # Default: Empty 51 | # To control custom models, use + to add a custom model, use - to hide a model, use name=displayName to customize model name, separated by comma. 52 | CUSTOM_MODELS= 53 | 54 | # (optional) 55 | # Default: Empty 56 | # Change default model 57 | DEFAULT_MODEL= 58 | 59 | # anthropic claude Api Key.(optional) 60 | ANTHROPIC_API_KEY= 61 | 62 | ### anthropic claude Api version. (optional) 63 | ANTHROPIC_API_VERSION= 64 | 65 | ### anthropic claude Api url (optional) 66 | ANTHROPIC_URL= 67 | 68 | ### (optional) 69 | WHITE_WEBDEV_ENDPOINTS= 70 | 71 | ### (optional) 72 | MJ_PROXY_KEY= 73 | 74 | ### (optional) 75 | MJ_PROXY_URL= 76 | MJ_PROXY_KEY= -------------------------------------------------------------------------------- /docs/cloudflare-pages-en.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages Deployment Guide 2 | 3 | ## How to create a new project 4 | 5 | Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. 6 | 7 | 1. Click "Create a project". 8 | 2. Choose "Connect to Git". 9 | 3. Connect Cloudflare Pages to your GitHub account. 10 | 4. Select the forked project. 11 | 5. Click "Begin setup". 12 | 6. For "Project name" and "Production branch", use the default values or change them as needed. 13 | 7. In "Build Settings", choose the "Framework presets" option and select "Next.js". 14 | 8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command: 15 | ``` 16 | npx @cloudflare/next-on-pages --experimental-minify 17 | ``` 18 | 9. For "Build output directory", use the default value and do not modify it. 19 | 10. Do not modify "Root Directory". 20 | 11. For "Environment variables", click ">" and then "Add variable". Fill in the following information: 21 | 22 | - `NODE_VERSION=20.1` 23 | - `NEXT_TELEMETRY_DISABLE=1` 24 | - `OPENAI_API_KEY=your_own_API_key` 25 | - `YARN_VERSION=1.22.19` 26 | - `PHP_VERSION=7.4` 27 | 28 | Optionally fill in the following based on your needs: 29 | 30 | - `CODE= Optional, access passwords, multiple passwords can be separated by commas` 31 | - `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI` 32 | - `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key` 33 | - `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4` 34 | - `ENABLE_BALANCE_QUERY=1 Optional, allow users to query balance` 35 | - `DISABLE_FAST_LINK=1 Optional, disable parse settings from url` 36 | - `OPENAI_SB=1 Optional,use the third-party OpenAI-SB API` 37 | 38 | 12. Click "Save and Deploy". 39 | 13. Click "Cancel deployment" because you need to fill in Compatibility flags. 40 | 14. Go to "Build settings", "Functions", and find "Compatibility flags". 41 | 15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag". 42 | 16. Go to "Deployments" and click "Retry deployment". 43 | 17. Enjoy. 44 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-es.md: -------------------------------------------------------------------------------- 1 | # Guía de implementación de Cloudflare Pages 2 | 3 | ## Cómo crear un nuevo proyecto 4 | 5 | Bifurca el proyecto en Github, luego inicia sesión en dash.cloudflare.com y ve a Pages. 6 | 7 | 1. Haga clic en "Crear un proyecto". 8 | 2. Selecciona Conectar a Git. 9 | 3. Vincula páginas de Cloudflare a tu cuenta de GitHub. 10 | 4. Seleccione este proyecto que bifurcó. 11 | 5. Haga clic en "Comenzar configuración". 12 | 6. Para "Nombre del proyecto" y "Rama de producción", puede utilizar los valores predeterminados o cambiarlos según sea necesario. 13 | 7. En Configuración de compilación, seleccione la opción Ajustes preestablecidos de Framework y seleccione Siguiente.js. 14 | 8. Debido a los errores de node:buffer, no use el "comando Construir" predeterminado por ahora. Utilice el siguiente comando: 15 | ``` 16 | npx @cloudflare/next-on-pages --experimental-minify 17 | ``` 18 | 9. Para "Generar directorio de salida", utilice los valores predeterminados y no los modifique. 19 | 10. No modifique el "Directorio raíz". 20 | 11. Para "Variables de entorno", haga clic en ">" y luego haga clic en "Agregar variable". Rellene la siguiente información: 21 | 22 | * `NODE_VERSION=20.1` 23 | * `NEXT_TELEMETRY_DISABLE=1` 24 | * `OPENAI_API_KEY=你自己的API Key` 25 | * `YARN_VERSION=1.22.19` 26 | * `PHP_VERSION=7.4` 27 | 28 | Dependiendo de sus necesidades reales, puede completar opcionalmente las siguientes opciones: 29 | 30 | * `CODE= 可选填,访问密码,可以使用逗号隔开多个密码` 31 | * `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID` 32 | * `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key` 33 | * `DISABLE_GPT4=1 可选,不让用户使用 GPT-4` 34 | 12. Haga clic en "Guardar e implementar". 35 | 13. Haga clic en "Cancelar implementación" porque necesita rellenar los indicadores de compatibilidad. 36 | 14. Vaya a "Configuración de compilación", "Funciones" y busque "Indicadores de compatibilidad". 37 | 15. Rellene "nodejs_compat" en "Configurar indicador de compatibilidad de producción" y "Configurar indicador de compatibilidad de vista previa". 38 | 16. Vaya a "Implementaciones" y haga clic en "Reintentar implementación". 39 | 17. Disfrutar. 40 | -------------------------------------------------------------------------------- /app/command.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useSearchParams } from "react-router-dom"; 3 | import Locale from "./locales"; 4 | 5 | type Command = (param: string) => void; 6 | interface Commands { 7 | fill?: Command; 8 | submit?: Command; 9 | mask?: Command; 10 | code?: Command; 11 | settings?: Command; 12 | } 13 | 14 | export function useCommand(commands: Commands = {}) { 15 | const [searchParams, setSearchParams] = useSearchParams(); 16 | 17 | useEffect(() => { 18 | let shouldUpdate = false; 19 | searchParams.forEach((param, name) => { 20 | const commandName = name as keyof Commands; 21 | if (typeof commands[commandName] === "function") { 22 | commands[commandName]!(param); 23 | searchParams.delete(name); 24 | shouldUpdate = true; 25 | } 26 | }); 27 | 28 | if (shouldUpdate) { 29 | setSearchParams(searchParams); 30 | } 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, [searchParams, commands]); 33 | } 34 | 35 | interface ChatCommands { 36 | new?: Command; 37 | newm?: Command; 38 | next?: Command; 39 | prev?: Command; 40 | clear?: Command; 41 | del?: Command; 42 | } 43 | 44 | export const ChatCommandPrefix = ":"; 45 | 46 | export function useChatCommand(commands: ChatCommands = {}) { 47 | function extract(userInput: string) { 48 | return ( 49 | userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput 50 | ) as keyof ChatCommands; 51 | } 52 | 53 | function search(userInput: string) { 54 | const input = extract(userInput); 55 | const desc = Locale.Chat.Commands; 56 | return Object.keys(commands) 57 | .filter((c) => c.startsWith(input)) 58 | .map((c) => ({ 59 | title: desc[c as keyof ChatCommands], 60 | content: ChatCommandPrefix + c, 61 | })); 62 | } 63 | 64 | function match(userInput: string) { 65 | const command = extract(userInput); 66 | const matched = typeof commands[command] === "function"; 67 | 68 | return { 69 | matched, 70 | invoke: () => matched && commands[command]!(userInput), 71 | }; 72 | } 73 | 74 | return { match, search }; 75 | } 76 | -------------------------------------------------------------------------------- /app/icons/image.svg: -------------------------------------------------------------------------------- 1 | Layer 1 -------------------------------------------------------------------------------- /app/api/upstash/[action]/[...key]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | async function handle( 4 | req: NextRequest, 5 | { params }: { params: { action: string; key: string[] } }, 6 | ) { 7 | const requestUrl = new URL(req.url); 8 | const endpoint = requestUrl.searchParams.get("endpoint"); 9 | 10 | if (req.method === "OPTIONS") { 11 | return NextResponse.json({ body: "OK" }, { status: 200 }); 12 | } 13 | const [...key] = params.key; 14 | // only allow to request to *.upstash.io 15 | if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) { 16 | return NextResponse.json( 17 | { 18 | error: true, 19 | msg: "you are not allowed to request " + params.key.join("/"), 20 | }, 21 | { 22 | status: 403, 23 | }, 24 | ); 25 | } 26 | 27 | // only allow upstash get and set method 28 | if (params.action !== "get" && params.action !== "set") { 29 | console.log("[Upstash Route] forbidden action ", params.action); 30 | return NextResponse.json( 31 | { 32 | error: true, 33 | msg: "you are not allowed to request " + params.action, 34 | }, 35 | { 36 | status: 403, 37 | }, 38 | ); 39 | } 40 | 41 | const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`; 42 | 43 | const method = req.method; 44 | const shouldNotHaveBody = ["get", "head"].includes( 45 | method?.toLowerCase() ?? "", 46 | ); 47 | 48 | const fetchOptions: RequestInit = { 49 | headers: { 50 | authorization: req.headers.get("authorization") ?? "", 51 | }, 52 | body: shouldNotHaveBody ? null : req.body, 53 | method, 54 | // @ts-ignore 55 | duplex: "half", 56 | }; 57 | 58 | console.log("[Upstash Proxy]", targetUrl, fetchOptions); 59 | const fetchResult = await fetch(targetUrl, fetchOptions); 60 | 61 | console.log("[Any Proxy]", targetUrl, { 62 | status: fetchResult.status, 63 | statusText: fetchResult.statusText, 64 | }); 65 | 66 | return fetchResult; 67 | } 68 | 69 | export const POST = handle; 70 | export const GET = handle; 71 | export const OPTIONS = handle; 72 | 73 | export const runtime = "edge"; 74 | -------------------------------------------------------------------------------- /app/icons/confirm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/vercel-es.md: -------------------------------------------------------------------------------- 1 | # Instrucciones de uso de Verbel 2 | 3 | ## Cómo crear un nuevo proyecto 4 | 5 | Cuando bifurca este proyecto desde Github y necesita crear un nuevo proyecto de Vercel en Vercel para volver a implementarlo, debe seguir los pasos a continuación. 6 | 7 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 8 | 9 | 1. Vaya a la página de inicio de la consola de Vercel; 10 | 2. Haga clic en Agregar nuevo; 11 | 3. Seleccione Proyecto. 12 | 13 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 14 | 15 | 1. En Import Git Repository, busque chatgpt-next-web; 16 | 2. Seleccione el proyecto de la nueva bifurcación y haga clic en Importar. 17 | 18 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 19 | 20 | 1. En la página de configuración del proyecto, haga clic en Variables de entorno para configurar las variables de entorno; 21 | 2. Agregar variables de entorno denominadas OPENAI_API_KEY y CODE; 22 | 3. Rellenar los valores correspondientes a las variables de entorno; 23 | 4. Haga clic en Agregar para confirmar la adición de variables de entorno; 24 | 5. Asegúrese de agregar OPENAI_API_KEY, de lo contrario no funcionará; 25 | 6. Haga clic en Implementar, créelo y espere pacientemente unos 5 minutos a que se complete la implementación. 26 | 27 | ## Cómo agregar un nombre de dominio personalizado 28 | 29 | \[TODO] 30 | 31 | ## Cómo cambiar las variables de entorno 32 | 33 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 34 | 35 | 1. Vaya a la consola interna del proyecto Vercel y haga clic en el botón Configuración en la parte superior; 36 | 2. Haga clic en Variables de entorno a la izquierda; 37 | 3. Haga clic en el botón a la derecha de una entrada existente; 38 | 4. Seleccione Editar para editarlo y, a continuación, guárdelo. 39 | 40 | ⚠️️ Nota: Lo necesita cada vez que modifique las variables de entorno[Volver a implementar el proyecto](#如何重新部署)para que los cambios surtan efecto! 41 | 42 | ## Cómo volver a implementar 43 | 44 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 45 | 46 | 1. Vaya a la consola interna del proyecto Vercel y haga clic en el botón Implementaciones en la parte superior; 47 | 2. Seleccione el botón derecho del artículo superior de la lista; 48 | 3. Haga clic en Volver a implementar para volver a implementar. 49 | -------------------------------------------------------------------------------- /public/serviceWorker.js: -------------------------------------------------------------------------------- 1 | const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache"; 2 | const CHATGPT_NEXT_WEB_FILE_CACHE = "chatgpt-next-web-file"; 3 | let a="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";let nanoid=(e=21)=>{let t="",r=crypto.getRandomValues(new Uint8Array(e));for(let n=0;n { 50 | const url = new URL(e.request.url); 51 | if (/^\/api\/cache/.test(url.pathname)) { 52 | if ('GET' == e.request.method) { 53 | e.respondWith(caches.match(e.request)) 54 | } 55 | if ('POST' == e.request.method) { 56 | e.respondWith(upload(e.request, url)) 57 | } 58 | if ('DELETE' == e.request.method) { 59 | e.respondWith(remove(e.request, url)) 60 | } 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /app/components/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { IconButton } from "./button"; 5 | import GithubIcon from "../icons/github.svg"; 6 | import ResetIcon from "../icons/reload.svg"; 7 | import { ISSUE_URL } from "../constant"; 8 | import Locale from "../locales"; 9 | import { showConfirm } from "./ui-lib"; 10 | import { useSyncStore } from "../store/sync"; 11 | 12 | interface IErrorBoundaryState { 13 | hasError: boolean; 14 | error: Error | null; 15 | info: React.ErrorInfo | null; 16 | } 17 | 18 | export class ErrorBoundary extends React.Component { 19 | constructor(props: any) { 20 | super(props); 21 | this.state = { hasError: false, error: null, info: null }; 22 | } 23 | 24 | componentDidCatch(error: Error, info: React.ErrorInfo) { 25 | // Update state with error details 26 | this.setState({ hasError: true, error, info }); 27 | } 28 | 29 | clearAndSaveData() { 30 | try { 31 | useSyncStore.getState().export(); 32 | } finally { 33 | localStorage.clear(); 34 | location.reload(); 35 | } 36 | } 37 | 38 | render() { 39 | if (this.state.hasError) { 40 | // Render error message 41 | return ( 42 |
43 |

Oops, something went wrong!

44 |
45 |             {this.state.error?.toString()}
46 |             {this.state.info?.componentStack}
47 |           
48 | 49 |
50 | 51 | } 54 | bordered 55 | /> 56 | 57 | } 59 | text="Clear All Data" 60 | onClick={async () => { 61 | if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) { 62 | this.clearAndSaveData(); 63 | } 64 | }} 65 | bordered 66 | /> 67 |
68 |
69 | ); 70 | } 71 | // if no error occurred, render children 72 | return this.props.children; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/styles/highlight.scss: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | pre { 3 | padding: 0; 4 | } 5 | 6 | pre, 7 | code { 8 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; 9 | } 10 | 11 | pre code { 12 | display: block; 13 | overflow-x: auto; 14 | padding: 1em; 15 | } 16 | 17 | code { 18 | padding: 3px 5px; 19 | } 20 | 21 | .hljs, 22 | pre { 23 | background: #1a1b26; 24 | color: #cbd2ea; 25 | } 26 | 27 | /*! 28 | Theme: Tokyo-night-Dark 29 | origin: https://github.com/enkia/tokyo-night-vscode-theme 30 | Description: Original highlight.js style 31 | Author: (c) Henri Vandersleyen 32 | License: see project LICENSE 33 | Touched: 2022 34 | */ 35 | .hljs-comment, 36 | .hljs-meta { 37 | color: #565f89; 38 | } 39 | 40 | .hljs-deletion, 41 | .hljs-doctag, 42 | .hljs-regexp, 43 | .hljs-selector-attr, 44 | .hljs-selector-class, 45 | .hljs-selector-id, 46 | .hljs-selector-pseudo, 47 | .hljs-tag, 48 | .hljs-template-tag, 49 | .hljs-variable.language_ { 50 | color: #f7768e; 51 | } 52 | 53 | .hljs-link, 54 | .hljs-literal, 55 | .hljs-number, 56 | .hljs-params, 57 | .hljs-template-variable, 58 | .hljs-type, 59 | .hljs-variable { 60 | color: #ff9e64; 61 | } 62 | 63 | .hljs-attribute, 64 | .hljs-built_in { 65 | color: #e0af68; 66 | } 67 | 68 | .hljs-keyword, 69 | .hljs-property, 70 | .hljs-subst, 71 | .hljs-title, 72 | .hljs-title.class_, 73 | .hljs-title.class_.inherited__, 74 | .hljs-title.function_ { 75 | color: #7dcfff; 76 | } 77 | 78 | .hljs-selector-tag { 79 | color: #73daca; 80 | } 81 | 82 | .hljs-addition, 83 | .hljs-bullet, 84 | .hljs-quote, 85 | .hljs-string, 86 | .hljs-symbol { 87 | color: #9ece6a; 88 | } 89 | 90 | .hljs-code, 91 | .hljs-formula, 92 | .hljs-section { 93 | color: #7aa2f7; 94 | } 95 | 96 | .hljs-attr, 97 | .hljs-char.escape_, 98 | .hljs-keyword, 99 | .hljs-name, 100 | .hljs-operator { 101 | color: #bb9af7; 102 | } 103 | 104 | .hljs-punctuation { 105 | color: #c0caf5; 106 | } 107 | 108 | .hljs-emphasis { 109 | font-style: italic; 110 | } 111 | 112 | .hljs-strong { 113 | font-weight: 700; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/api/artifacts/route.ts: -------------------------------------------------------------------------------- 1 | import md5 from "spark-md5"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import { getServerSideConfig } from "@/app/config/server"; 4 | 5 | async function handle(req: NextRequest, res: NextResponse) { 6 | const serverConfig = getServerSideConfig(); 7 | const storeUrl = () => 8 | `https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`; 9 | const storeHeaders = () => ({ 10 | Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`, 11 | }); 12 | if (req.method === "POST") { 13 | const clonedBody = await req.text(); 14 | const hashedCode = md5.hash(clonedBody).trim(); 15 | const body: { 16 | key: string; 17 | value: string; 18 | expiration_ttl?: number; 19 | } = { 20 | key: hashedCode, 21 | value: clonedBody, 22 | }; 23 | try { 24 | const ttl = parseInt(serverConfig.cloudflareKVTTL as string); 25 | if (ttl > 60) { 26 | body["expiration_ttl"] = ttl; 27 | } 28 | } catch (e) { 29 | console.error(e); 30 | } 31 | const res = await fetch(`${storeUrl()}/bulk`, { 32 | headers: { 33 | ...storeHeaders(), 34 | "Content-Type": "application/json", 35 | }, 36 | method: "PUT", 37 | body: JSON.stringify([body]), 38 | }); 39 | const result = await res.json(); 40 | console.log("save data", result); 41 | if (result?.success) { 42 | return NextResponse.json( 43 | { code: 0, id: hashedCode, result }, 44 | { status: res.status }, 45 | ); 46 | } 47 | return NextResponse.json( 48 | { error: true, msg: "Save data error" }, 49 | { status: 400 }, 50 | ); 51 | } 52 | if (req.method === "GET") { 53 | const id = req?.nextUrl?.searchParams?.get("id"); 54 | const res = await fetch(`${storeUrl()}/values/${id}`, { 55 | headers: storeHeaders(), 56 | method: "GET", 57 | }); 58 | return new Response(res.body, { 59 | status: res.status, 60 | statusText: res.statusText, 61 | headers: res.headers, 62 | }); 63 | } 64 | return NextResponse.json( 65 | { error: true, msg: "Invalid request" }, 66 | { status: 400 }, 67 | ); 68 | } 69 | 70 | export const POST = handle; 71 | export const GET = handle; 72 | 73 | export const runtime = "edge"; 74 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if running on a supported system 4 | case "$(uname -s)" in 5 | Linux) 6 | if [[ -f "/etc/lsb-release" ]]; then 7 | . /etc/lsb-release 8 | if [[ "$DISTRIB_ID" != "Ubuntu" ]]; then 9 | echo "This script only works on Ubuntu, not $DISTRIB_ID." 10 | exit 1 11 | fi 12 | else 13 | if [[ !"$(cat /etc/*-release | grep '^ID=')" =~ ^(ID=\"ubuntu\")|(ID=\"centos\")|(ID=\"arch\")|(ID=\"debian\")$ ]]; then 14 | echo "Unsupported Linux distribution." 15 | exit 1 16 | fi 17 | fi 18 | ;; 19 | Darwin) 20 | echo "Running on MacOS." 21 | ;; 22 | *) 23 | echo "Unsupported operating system." 24 | exit 1 25 | ;; 26 | esac 27 | 28 | # Check if needed dependencies are installed and install if necessary 29 | if ! command -v node >/dev/null || ! command -v git >/dev/null || ! command -v yarn >/dev/null; then 30 | case "$(uname -s)" in 31 | Linux) 32 | if [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=ubuntu" ]]; then 33 | sudo apt-get update 34 | sudo apt-get -y install nodejs git yarn 35 | elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=debian" ]]; then 36 | sudo apt-get update 37 | sudo apt-get -y install nodejs git yarn 38 | elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=centos" ]]; then 39 | sudo yum -y install epel-release 40 | sudo yum -y install nodejs git yarn 41 | elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=arch" ]]; then 42 | sudo pacman -Syu -y 43 | sudo pacman -S -y nodejs git yarn 44 | else 45 | echo "Unsupported Linux distribution" 46 | exit 1 47 | fi 48 | ;; 49 | Darwin) 50 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 51 | brew install node git yarn 52 | ;; 53 | esac 54 | fi 55 | 56 | # Clone the repository and install dependencies 57 | git clone https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 58 | cd ChatGPT-Next-Web 59 | yarn install 60 | 61 | # Prompt user for environment variables 62 | read -p "Enter OPENAI_API_KEY: " OPENAI_API_KEY 63 | read -p "Enter CODE: " CODE 64 | read -p "Enter PORT: " PORT 65 | 66 | # Build and run the project using the environment variables 67 | OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn build 68 | OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn start 69 | -------------------------------------------------------------------------------- /app/components/button.module.scss: -------------------------------------------------------------------------------- 1 | .icon-button { 2 | background-color: var(--white); 3 | border-radius: 10px; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | padding: 10px; 8 | 9 | cursor: pointer; 10 | transition: all 0.3s ease; 11 | overflow: hidden; 12 | user-select: none; 13 | outline: none; 14 | border: none; 15 | color: var(--black); 16 | 17 | &[disabled] { 18 | cursor: not-allowed; 19 | opacity: 0.5; 20 | } 21 | 22 | &.primary { 23 | background-color: var(--primary); 24 | color: white; 25 | 26 | path { 27 | fill: white !important; 28 | } 29 | } 30 | 31 | &.danger { 32 | color: rgba($color: red, $alpha: 0.8); 33 | border-color: rgba($color: red, $alpha: 0.5); 34 | background-color: rgba($color: red, $alpha: 0.05); 35 | 36 | &:hover { 37 | border-color: red; 38 | background-color: rgba($color: red, $alpha: 0.1); 39 | } 40 | 41 | path { 42 | fill: red !important; 43 | } 44 | } 45 | 46 | &:hover, 47 | &:focus { 48 | border-color: var(--primary); 49 | } 50 | 51 | &.success { 52 | color: rgba($color: green, $alpha: 0.8); 53 | border-color: rgba($color: green, $alpha: 0.5); 54 | background-color: rgba($color: green, $alpha: 0.05); 55 | 56 | &:hover { 57 | border-color: green; 58 | background-color: rgba($color: green, $alpha: 0.1); 59 | } 60 | 61 | path { 62 | fill: green !important; 63 | } 64 | } 65 | 66 | &.purple { 67 | color: rgba($color: purple, $alpha: 0.8); 68 | border-color: rgba($color: purple, $alpha: 0.5); 69 | background-color: rgba($color: purple, $alpha: 0.05); 70 | 71 | &:hover { 72 | border-color: purple; 73 | background-color: rgba($color: purple, $alpha: 0.1); 74 | } 75 | 76 | path { 77 | fill: purple !important; 78 | } 79 | } 80 | } 81 | 82 | .shadow { 83 | box-shadow: var(--card-shadow); 84 | } 85 | 86 | .border { 87 | border: var(--border-in-light); 88 | } 89 | 90 | .icon-button-icon { 91 | width: 16px; 92 | height: 16px; 93 | display: flex; 94 | justify-content: center; 95 | align-items: center; 96 | } 97 | 98 | @media only screen and (max-width: 600px) { 99 | .icon-button { 100 | padding: 16px; 101 | } 102 | } 103 | 104 | .icon-button-text { 105 | font-size: 12px; 106 | overflow: hidden; 107 | text-overflow: ellipsis; 108 | white-space: nowrap; 109 | 110 | &:not(:first-child) { 111 | margin-left: 5px; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/api/openai/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import { type OpenAIListModelResponse } from "@/app/client/platforms/openai"; 2 | import { getServerSideConfig } from "@/app/config/server"; 3 | import { ModelProvider, OpenaiPath } from "@/app/constant"; 4 | import { prettyObject } from "@/app/utils/format"; 5 | import { NextRequest, NextResponse } from "next/server"; 6 | import { auth } from "../../auth"; 7 | import { requestOpenai } from "../../common"; 8 | 9 | const ALLOWD_PATH = new Set(Object.values(OpenaiPath)); 10 | 11 | function getModels(remoteModelRes: OpenAIListModelResponse) { 12 | const config = getServerSideConfig(); 13 | 14 | if (config.disableGPT4) { 15 | remoteModelRes.data = remoteModelRes.data.filter( 16 | (m) => !m.id.startsWith("gpt-4"), 17 | ); 18 | } 19 | 20 | return remoteModelRes; 21 | } 22 | 23 | async function handle( 24 | req: NextRequest, 25 | { params }: { params: { path: string[] } }, 26 | ) { 27 | console.log("[OpenAI Route] params ", params); 28 | 29 | if (req.method === "OPTIONS") { 30 | return NextResponse.json({ body: "OK" }, { status: 200 }); 31 | } 32 | 33 | const subpath = params.path.join("/"); 34 | 35 | if (!ALLOWD_PATH.has(subpath)) { 36 | console.log("[OpenAI Route] forbidden path ", subpath); 37 | return NextResponse.json( 38 | { 39 | error: true, 40 | msg: "you are not allowed to request " + subpath, 41 | }, 42 | { 43 | status: 403, 44 | }, 45 | ); 46 | } 47 | 48 | const authResult = auth(req, ModelProvider.GPT); 49 | if (authResult.error) { 50 | return NextResponse.json(authResult, { 51 | status: 401, 52 | }); 53 | } 54 | 55 | try { 56 | const response = await requestOpenai(req); 57 | 58 | // list models 59 | if (subpath === OpenaiPath.ListModelPath && response.status === 200) { 60 | const resJson = (await response.json()) as OpenAIListModelResponse; 61 | const availableModels = getModels(resJson); 62 | return NextResponse.json(availableModels, { 63 | status: response.status, 64 | }); 65 | } 66 | 67 | return response; 68 | } catch (e) { 69 | console.error("[OpenAI] ", e); 70 | return NextResponse.json(prettyObject(e)); 71 | } 72 | } 73 | 74 | export const GET = handle; 75 | export const POST = handle; 76 | 77 | export const runtime = "edge"; 78 | export const preferredRegion = [ 79 | "arn1", 80 | "bom1", 81 | "cdg1", 82 | "cle1", 83 | "cpt1", 84 | "dub1", 85 | "fra1", 86 | "gru1", 87 | "hnd1", 88 | "iad1", 89 | "icn1", 90 | "kix1", 91 | "lhr1", 92 | "pdx1", 93 | "sfo1", 94 | "sin1", 95 | "syd1", 96 | ]; 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatany", 3 | "private": false, 4 | "license": "mit", 5 | "scripts": { 6 | "mask": "npx tsx app/masks/build.ts", 7 | "mask:watch": "npx watch 'yarn mask' app/masks", 8 | "dev": "yarn run mask:watch & next dev", 9 | "build": "yarn mask && cross-env BUILD_MODE=standalone next build", 10 | "start": "next start", 11 | "lint": "next lint", 12 | "export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build", 13 | "export:dev": "yarn mask:watch & cross-env BUILD_MODE=export BUILD_APP=1 next dev", 14 | "app:dev": "yarn mask:watch & yarn tauri dev", 15 | "app:build": "yarn mask && yarn tauri build", 16 | "prompts": "node ./scripts/fetch-prompts.mjs", 17 | "prepare": "husky install", 18 | "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev" 19 | }, 20 | "dependencies": { 21 | "@fortaine/fetch-event-source": "^3.0.6", 22 | "@hello-pangea/dnd": "^16.5.0", 23 | "@next/third-parties": "^14.1.0", 24 | "@svgr/webpack": "^6.5.1", 25 | "@vercel/analytics": "^0.1.11", 26 | "@vercel/speed-insights": "^1.0.2", 27 | "deepcopy": "^2.1.0", 28 | "emoji-picker-react": "^4.9.2", 29 | "fuse.js": "^7.0.0", 30 | "heic2any": "^0.0.4", 31 | "html-to-image": "^1.11.11", 32 | "mermaid": "^10.6.1", 33 | "nanoid": "^5.0.3", 34 | "next": "^14.1.1", 35 | "node-fetch": "^3.3.1", 36 | "react": "^18.2.0", 37 | "react-dom": "^18.2.0", 38 | "react-markdown": "^8.0.7", 39 | "react-mask-editor": "^0.0.2", 40 | "react-router-dom": "^6.15.0", 41 | "rehype-highlight": "^6.0.0", 42 | "rehype-katex": "^6.0.3", 43 | "remark-breaks": "^3.0.2", 44 | "remark-gfm": "^3.0.1", 45 | "remark-math": "^5.1.1", 46 | "sass": "^1.59.2", 47 | "spark-md5": "^3.0.2", 48 | "use-debounce": "^9.0.4", 49 | "zustand": "^4.3.8" 50 | }, 51 | "devDependencies": { 52 | "@tauri-apps/cli": "1.5.11", 53 | "@types/node": "^20.11.30", 54 | "@types/react": "^18.2.70", 55 | "@types/react-dom": "^18.2.7", 56 | "@types/react-katex": "^3.0.0", 57 | "@types/spark-md5": "^3.0.4", 58 | "cross-env": "^7.0.3", 59 | "eslint": "^8.49.0", 60 | "eslint-config-next": "13.4.19", 61 | "eslint-config-prettier": "^8.8.0", 62 | "eslint-plugin-prettier": "^5.1.3", 63 | "husky": "^8.0.0", 64 | "lint-staged": "^13.2.2", 65 | "prettier": "^3.0.2", 66 | "tsx": "^4.16.0", 67 | "typescript": "5.2.2", 68 | "watch": "^1.0.2", 69 | "webpack": "^5.88.1" 70 | }, 71 | "resolutions": { 72 | "lint-staged/yaml": "^2.2.2" 73 | }, 74 | "packageManager": "yarn@1.22.19" 75 | } 76 | -------------------------------------------------------------------------------- /app/utils/cloud/webdav.ts: -------------------------------------------------------------------------------- 1 | import { STORAGE_KEY } from "@/app/constant"; 2 | import { SyncStore } from "@/app/store/sync"; 3 | 4 | export type WebDAVConfig = SyncStore["webdav"]; 5 | export type WebDavClient = ReturnType; 6 | 7 | export function createWebDavClient(store: SyncStore) { 8 | const folder = STORAGE_KEY; 9 | const fileName = `${folder}/backup.json`; 10 | const config = store.webdav; 11 | const proxyUrl = 12 | store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined; 13 | 14 | return { 15 | async check() { 16 | try { 17 | const res = await fetch(this.path(folder, proxyUrl), { 18 | method: "MKCOL", 19 | headers: this.headers(), 20 | }); 21 | const success = [201, 200, 404, 405, 301, 302, 307, 308].includes( 22 | res.status, 23 | ); 24 | console.log( 25 | `[WebDav] check ${success ? "success" : "failed"}, ${res.status} ${ 26 | res.statusText 27 | }`, 28 | ); 29 | return success; 30 | } catch (e) { 31 | console.error("[WebDav] failed to check", e); 32 | } 33 | 34 | return false; 35 | }, 36 | 37 | async get(key: string) { 38 | const res = await fetch(this.path(fileName, proxyUrl), { 39 | method: "GET", 40 | headers: this.headers(), 41 | }); 42 | 43 | console.log("[WebDav] get key = ", key, res.status, res.statusText); 44 | 45 | return await res.text(); 46 | }, 47 | 48 | async set(key: string, value: string) { 49 | const res = await fetch(this.path(fileName, proxyUrl), { 50 | method: "PUT", 51 | headers: this.headers(), 52 | body: value, 53 | }); 54 | 55 | console.log("[WebDav] set key = ", key, res.status, res.statusText); 56 | }, 57 | 58 | headers() { 59 | const auth = btoa(config.username + ":" + config.password); 60 | 61 | return { 62 | authorization: `Basic ${auth}`, 63 | }; 64 | }, 65 | path(path: string, proxyUrl: string = "") { 66 | if (path.startsWith("/")) { 67 | path = path.slice(1); 68 | } 69 | 70 | if (proxyUrl.endsWith("/")) { 71 | proxyUrl = proxyUrl.slice(0, -1); 72 | } 73 | 74 | let url; 75 | const pathPrefix = "/api/webdav/"; 76 | 77 | try { 78 | let u = new URL(proxyUrl + pathPrefix + path); 79 | // add query params 80 | u.searchParams.append("endpoint", config.endpoint); 81 | url = u.toString(); 82 | } catch (e) { 83 | url = pathPrefix + path + "?endpoint=" + config.endpoint; 84 | } 85 | 86 | return url; 87 | }, 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /app/components/mask.module.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/animation.scss"; 2 | .mask-page { 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | 7 | .mask-page-body { 8 | padding: 20px; 9 | overflow-y: auto; 10 | 11 | .mask-filter { 12 | width: 100%; 13 | max-width: 100%; 14 | margin-bottom: 20px; 15 | animation: slide-in ease 0.3s; 16 | height: 40px; 17 | 18 | display: flex; 19 | 20 | .search-bar { 21 | flex-grow: 1; 22 | max-width: 100%; 23 | min-width: 0; 24 | } 25 | 26 | .mask-filter-lang { 27 | height: 100%; 28 | margin-left: 10px; 29 | } 30 | 31 | .mask-create { 32 | height: 100%; 33 | margin-left: 10px; 34 | box-sizing: border-box; 35 | min-width: 80px; 36 | } 37 | } 38 | 39 | .mask-item { 40 | display: flex; 41 | justify-content: space-between; 42 | padding: 20px; 43 | border: var(--border-in-light); 44 | animation: slide-in ease 0.3s; 45 | 46 | &:not(:last-child) { 47 | border-bottom: 0; 48 | } 49 | 50 | &:first-child { 51 | border-top-left-radius: 10px; 52 | border-top-right-radius: 10px; 53 | } 54 | 55 | &:last-child { 56 | border-bottom-left-radius: 10px; 57 | border-bottom-right-radius: 10px; 58 | } 59 | 60 | .mask-header { 61 | display: flex; 62 | align-items: center; 63 | 64 | .mask-icon { 65 | display: flex; 66 | align-items: center; 67 | justify-content: center; 68 | margin-right: 10px; 69 | } 70 | 71 | .mask-title { 72 | .mask-name { 73 | font-size: 14px; 74 | font-weight: bold; 75 | } 76 | .mask-info { 77 | font-size: 12px; 78 | } 79 | } 80 | } 81 | 82 | .mask-actions { 83 | display: flex; 84 | flex-wrap: nowrap; 85 | transition: all ease 0.3s; 86 | } 87 | 88 | @media screen and (max-width: 600px) { 89 | display: flex; 90 | flex-direction: column; 91 | padding-bottom: 10px; 92 | border-radius: 10px; 93 | margin-bottom: 20px; 94 | box-shadow: var(--card-shadow); 95 | 96 | &:not(:last-child) { 97 | border-bottom: var(--border-in-light); 98 | } 99 | 100 | .mask-actions { 101 | width: 100%; 102 | justify-content: space-between; 103 | padding-top: 10px; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/icons/light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/user-manual-cn.md: -------------------------------------------------------------------------------- 1 | # 用户手册 User Manual 2 | 3 | > No english version yet, please read this doc with ChatGPT or other translation tools. 4 | 5 | 本文档用于解释 NextChat 的部分功能介绍和设计原则。 6 | 7 | ## 面具 (Mask) 8 | 9 | ### 什么是面具?它和提示词的区别是什么? 10 | 11 | 面具 = 多个预设提示词 + 模型设置 + 对话设置。 12 | 13 | 其中预设提示词(Contextual Prompts)一般用于 In-Context Learning,用于让 ChatGPT 生成更加符合要求的输出,也可以增加系统约束或者输入有限的额外知识。 14 | 15 | 模型设置则顾名思义,使用此面具创建的对话都会默认使用对应的模型参数。 16 | 17 | 对话设置是与对话体验相关的一系列设置,我们会在下方的章节中依次介绍。 18 | 19 | ### 如何添加一个预设面具? 20 | 21 | 目前仅能够通过编辑源代码的方式添加预设面具,请根据需要编辑 [mask](../app/masks/) 目录下对应语言的文件即可。 22 | 23 | 编辑步骤如下: 24 | 25 | 1. 在 NextChat 中配置好一个面具; 26 | 2. 使用面具编辑页面的下载按钮,将面具保存为 JSON 格式; 27 | 3. 让 ChatGPT 帮你将 json 文件格式化为对应的 ts 代码; 28 | 4. 放入对应的 .ts 文件。 29 | 30 | 后续会增加使用旁加载的方式加载面具。 31 | 32 | ## 对话 (Chat) 33 | 34 | ### 对话框上方的按钮的作用 35 | 36 | 在默认状态下,将鼠标移动到按钮上,即可查看按钮的文字说明,我们依次介绍: 37 | 38 | - 对话设置:当前对话的设置,它与全局设置的关系,请查看下一小节的说明; 39 | - 颜色主题:点击即可在自动、暗黑、浅色之间轮换; 40 | - 快捷指令:项目内置的快捷填充预设提示词,也可以在对话框中输入 / 进行搜索; 41 | - 所有面具:进入面具页面; 42 | - 清除聊天:插入一个清除标记,标记上方的聊天将不会发给 GPT,效果相当于清除了当前对话,当然,你也可以再次点击该按钮,可取消清除; 43 | - 模型设置:更改当前对话的模型,注意,此按钮只会修改当前对话的模型,并不会修改全局默认模型。 44 | 45 | ### 对话内设置与全局设置的关系 46 | 47 | 目前有两处设置入口: 48 | 49 | 1. 页面左下角的设置按钮,进入后是全局设置页; 50 | 2. 对话框上方的设置按钮,进入后是对话设置页。 51 | 52 | 在新建对话后,该对话的设置默认与全局设置保持同步,修改全局设置,则新建对话的对话内设置也会被同步修改。 53 | 54 | 一旦用户手动更改过对话内设置,则对话内设置将与全局设置断开同步,此时更改全局设置,将不会对该对话生效。 55 | 56 | 如果想恢复两者的同步关系,可以将“对话内设置 -> 使用全局设置”选项勾选。 57 | 58 | ### 对话内设置项的含义 59 | 60 | 点开对话框上方的按钮,进入对话内设置,内容从上到下依次为: 61 | 62 | - 预设提示词列表:可以增加、删除、排序预设提示词 63 | - 角色头像:顾名思义 64 | - 角色名称:顾名思义 65 | - 隐藏预设对话:隐藏后,预设提示词不会出现在聊天界面 66 | - 使用全局设置:用于表示当前对话是否使用全局对话设置 67 | - 模型设置选项:剩余的选项与全局设置选项含义一致,见下一小节 68 | 69 | ### 全局设置项的含义 70 | 71 | - model / temperature / top_p / max_tokens / presence_penalty / frequency_penalty 均为 ChatGPT 的设置参数,详情请查阅 OpenAI 官方文档,再次不再赘述; 72 | - 注入系统级提示信息、用户输入预处理:详情请看 [https://github.com/Yidadaa/ChatGPT-Next-Web/issues/2144](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/2144) 73 | - 附带历史消息数:用户每次输入消息并发送时,所携带的最近 n 条消息数量; 74 | - 历史消息长度压缩阈值:当已经产生的聊天字数达到该数值以后,则自动触发历史摘要功能; 75 | - 历史摘要:是否启用历史摘要功能。 76 | 77 | ### 什么是历史摘要? 78 | 79 | 历史摘要功能,也是历史消息压缩功能,是保证长对话场景下保持历史记忆的关键,合理使用该功能可以在不丢失历史话题信息的情况下,节省所使用的 token。 80 | 81 | 由于 ChatGPT API 的长度限制,我们以 3.5 模型为例,它只能接受小于 4096 tokens 的对话消息,一旦超出这个数值,就会报错。 82 | 83 | 同时为了让 ChatGPT 理解我们对话的上下文,往往会携带多条历史消息来提供上下文信息,而当对话进行一段时间之后,很容易就会触发长度限制。 84 | 85 | 为了解决此问题,我们增加了历史记录压缩功能,假设阈值为 1000 字符,那么每次用户产生的聊天记录超过 1000 字符时,都会将没有被总结过的消息,发送给 ChatGPT,让其产生一个 100 字所有的摘要。 86 | 87 | 这样,历史信息就从 1000 字压缩到了 100 字,这是一种有损压缩,但已能满足大多数使用场景。 88 | 89 | ### 什么时候应该关闭历史摘要? 90 | 91 | 历史摘要可能会影响 ChatGPT 的对话质量,所以如果对话场景是翻译、信息提取等一次性对话场景,请直接关闭历史摘要功能,并将历史消息数设置为 0。 92 | 93 | ### 当用户发送一条消息时,有哪些信息被发送出去了? 94 | 95 | 当用户在对话框输入了一条消息后,发送给 ChatGPT 的消息,包含以下几个部分: 96 | 97 | 1. 系统级提示词:用于尽可能贴近 ChatGPT 官方 WebUI 的使用体验,可在设置中关闭此信息; 98 | 2. 历史摘要:作为长期记忆,提供长久但模糊的上下文信息; 99 | 3. 预设提示词:当前对话内设置的预设提示词,用于 In-Context Learning 或者注入系统级限制; 100 | 4. 最近 n 条对话记录:作为短期记忆,提供短暂但精确的上下文信息; 101 | 5. 用户当前输入的消息。 102 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import webpack from "webpack"; 2 | 3 | const mode = process.env.BUILD_MODE ?? "standalone"; 4 | console.log("[Next] build mode", mode); 5 | 6 | const disableChunk = !!process.env.DISABLE_CHUNK || mode === "export"; 7 | console.log("[Next] build with chunk: ", !disableChunk); 8 | 9 | /** @type {import('next').NextConfig} */ 10 | const nextConfig = { 11 | webpack(config) { 12 | config.module.rules.push({ 13 | test: /\.svg$/, 14 | use: ["@svgr/webpack"], 15 | }); 16 | 17 | if (disableChunk) { 18 | config.plugins.push( 19 | new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }), 20 | ); 21 | } 22 | 23 | config.resolve.fallback = { 24 | child_process: false, 25 | }; 26 | 27 | return config; 28 | }, 29 | output: mode, 30 | images: { 31 | unoptimized: mode === "export", 32 | }, 33 | experimental: { 34 | forceSwcTransforms: true, 35 | }, 36 | }; 37 | 38 | const CorsHeaders = [ 39 | { key: "Access-Control-Allow-Credentials", value: "true" }, 40 | { key: "Access-Control-Allow-Origin", value: "*" }, 41 | { 42 | key: "Access-Control-Allow-Methods", 43 | value: "*", 44 | }, 45 | { 46 | key: "Access-Control-Allow-Headers", 47 | value: "*", 48 | }, 49 | { 50 | key: "Access-Control-Max-Age", 51 | value: "86400", 52 | }, 53 | ]; 54 | 55 | if (mode !== "export") { 56 | nextConfig.headers = async () => { 57 | return [ 58 | { 59 | source: "/api/:path*", 60 | headers: CorsHeaders, 61 | }, 62 | ]; 63 | }; 64 | 65 | nextConfig.rewrites = async () => { 66 | const ret = [ 67 | // adjust for previous version directly using "/api/proxy/" as proxy base route 68 | { 69 | source: "/api/proxy/v1/:path*", 70 | destination: "https://api.openai.com/v1/:path*", 71 | }, 72 | { 73 | // https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions 74 | source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*", 75 | destination: "https://:resource_name.openai.azure.com/openai/deployments/:deploy_name/:path*", 76 | }, 77 | { 78 | source: "/api/proxy/google/:path*", 79 | destination: "https://generativelanguage.googleapis.com/:path*", 80 | }, 81 | { 82 | source: "/api/proxy/openai/:path*", 83 | destination: "https://api.openai.com/:path*", 84 | }, 85 | { 86 | source: "/api/proxy/anthropic/:path*", 87 | destination: "https://api.anthropic.com/:path*", 88 | }, 89 | { 90 | source: "/google-fonts/:path*", 91 | destination: "https://fonts.googleapis.com/:path*", 92 | }, 93 | { 94 | source: "/sharegpt", 95 | destination: "https://sharegpt.com/api/conversations", 96 | }, 97 | ]; 98 | 99 | return { 100 | beforeFiles: ret, 101 | }; 102 | }; 103 | } 104 | 105 | export default nextConfig; 106 | -------------------------------------------------------------------------------- /app/components/new-chat.module.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/animation.scss"; 2 | 3 | .new-chat { 4 | height: 100%; 5 | width: 100%; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | flex-direction: column; 10 | 11 | .mask-header { 12 | display: flex; 13 | justify-content: space-between; 14 | width: 100%; 15 | padding: 10px; 16 | box-sizing: border-box; 17 | animation: slide-in-from-top ease 0.3s; 18 | } 19 | 20 | .mask-cards { 21 | display: flex; 22 | margin-top: 5vh; 23 | margin-bottom: 20px; 24 | animation: slide-in ease 0.3s; 25 | 26 | .mask-card { 27 | padding: 20px 10px; 28 | border: var(--border-in-light); 29 | box-shadow: var(--card-shadow); 30 | border-radius: 14px; 31 | background-color: var(--white); 32 | transform: scale(1); 33 | 34 | &:first-child { 35 | transform: rotate(-15deg) translateY(5px); 36 | } 37 | 38 | &:last-child { 39 | transform: rotate(15deg) translateY(5px); 40 | } 41 | } 42 | } 43 | 44 | .title { 45 | font-size: 32px; 46 | font-weight: bolder; 47 | margin-bottom: 1vh; 48 | animation: slide-in ease 0.35s; 49 | } 50 | 51 | .sub-title { 52 | animation: slide-in ease 0.4s; 53 | } 54 | 55 | .actions { 56 | margin-top: 5vh; 57 | margin-bottom: 2vh; 58 | animation: slide-in ease 0.45s; 59 | display: flex; 60 | justify-content: center; 61 | font-size: 12px; 62 | 63 | .skip { 64 | margin-left: 10px; 65 | } 66 | } 67 | 68 | .masks { 69 | flex-grow: 1; 70 | width: 100%; 71 | overflow: auto; 72 | align-items: center; 73 | padding-top: 20px; 74 | 75 | $linear: linear-gradient( 76 | to bottom, 77 | rgba(0, 0, 0, 0), 78 | rgba(0, 0, 0, 1), 79 | rgba(0, 0, 0, 0) 80 | ); 81 | 82 | -webkit-mask-image: $linear; 83 | mask-image: $linear; 84 | 85 | animation: slide-in ease 0.5s; 86 | 87 | .mask-row { 88 | display: flex; 89 | // justify-content: center; 90 | margin-bottom: 10px; 91 | 92 | @for $i from 1 to 10 { 93 | &:nth-child(#{$i * 2}) { 94 | margin-left: 50px; 95 | } 96 | } 97 | 98 | .mask { 99 | display: flex; 100 | align-items: center; 101 | padding: 10px 14px; 102 | border: var(--border-in-light); 103 | box-shadow: var(--card-shadow); 104 | background-color: var(--white); 105 | border-radius: 10px; 106 | margin-right: 10px; 107 | max-width: 8em; 108 | transform: scale(1); 109 | cursor: pointer; 110 | transition: all ease 0.3s; 111 | 112 | &:hover { 113 | transform: translateY(-5px) scale(1.1); 114 | z-index: 999; 115 | border-color: var(--primary); 116 | } 117 | 118 | .mask-name { 119 | margin-left: 10px; 120 | font-size: 14px; 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/components/auth.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./auth.module.scss"; 2 | import { IconButton } from "./button"; 3 | 4 | import { useNavigate } from "react-router-dom"; 5 | import { Path } from "../constant"; 6 | import { useAccessStore } from "../store"; 7 | import Locale from "../locales"; 8 | 9 | import BotIcon from "../icons/bot.svg"; 10 | import { useEffect } from "react"; 11 | import { getClientConfig } from "../config/client"; 12 | 13 | export function AuthPage() { 14 | const navigate = useNavigate(); 15 | const accessStore = useAccessStore(); 16 | 17 | const goHome = () => navigate(Path.Home); 18 | const goChat = () => navigate(Path.Chat); 19 | const resetAccessCode = () => { 20 | accessStore.update((access) => { 21 | access.openaiApiKey = ""; 22 | access.accessCode = ""; 23 | }); 24 | }; // Reset access code to empty string 25 | 26 | useEffect(() => { 27 | if (getClientConfig()?.isApp) { 28 | navigate(Path.Settings); 29 | } 30 | // eslint-disable-next-line react-hooks/exhaustive-deps 31 | }, []); 32 | 33 | return ( 34 |
35 |
36 | 37 |
38 | 39 |
{Locale.Auth.Title}
40 |
{Locale.Auth.Tips}
41 | 42 | { 48 | accessStore.update( 49 | (access) => (access.accessCode = e.currentTarget.value), 50 | ); 51 | }} 52 | /> 53 | {!accessStore.hideUserApiKey ? ( 54 | <> 55 |
{Locale.Auth.SubTips}
56 | { 62 | accessStore.update( 63 | (access) => (access.openaiApiKey = e.currentTarget.value), 64 | ); 65 | }} 66 | /> 67 | { 73 | accessStore.update( 74 | (access) => (access.googleApiKey = e.currentTarget.value), 75 | ); 76 | }} 77 | /> 78 | 79 | ) : null} 80 | 81 |
82 | 87 | { 90 | resetAccessCode(); 91 | goHome(); 92 | }} 93 | /> 94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /scripts/fetch-prompts.mjs: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import fs from "fs/promises"; 3 | 4 | const RAW_FILE_URL = "https://raw.githubusercontent.com/"; 5 | const MIRRORF_FILE_URL = "http://raw.fgit.ml/"; 6 | 7 | const RAW_CN_URL = "PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json"; 8 | const CN_URL = MIRRORF_FILE_URL + RAW_CN_URL; 9 | const RAW_TW_URL = "PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh-TW.json"; 10 | const TW_URL = MIRRORF_FILE_URL + RAW_TW_URL; 11 | const RAW_EN_URL = "f/awesome-chatgpt-prompts/main/prompts.csv"; 12 | const EN_URL = MIRRORF_FILE_URL + RAW_EN_URL; 13 | const FILE = "./public/prompts.json"; 14 | 15 | const ignoreWords = ["涩涩", "魅魔", "澀澀"]; 16 | 17 | const timeoutPromise = (timeout) => { 18 | return new Promise((resolve, reject) => { 19 | setTimeout(() => { 20 | reject(new Error("Request timeout")); 21 | }, timeout); 22 | }); 23 | }; 24 | 25 | async function fetchCN() { 26 | console.log("[Fetch] fetching cn prompts..."); 27 | try { 28 | const response = await Promise.race([fetch(CN_URL), timeoutPromise(5000)]); 29 | const raw = await response.json(); 30 | return raw 31 | .map((v) => [v.act, v.prompt]) 32 | .filter( 33 | (v) => 34 | v[0] && 35 | v[1] && 36 | ignoreWords.every((w) => !v[0].includes(w) && !v[1].includes(w)), 37 | ); 38 | } catch (error) { 39 | console.error("[Fetch] failed to fetch cn prompts", error); 40 | return []; 41 | } 42 | } 43 | 44 | async function fetchTW() { 45 | console.log("[Fetch] fetching tw prompts..."); 46 | try { 47 | const response = await Promise.race([fetch(TW_URL), timeoutPromise(5000)]); 48 | const raw = await response.json(); 49 | return raw 50 | .map((v) => [v.act, v.prompt]) 51 | .filter( 52 | (v) => 53 | v[0] && 54 | v[1] && 55 | ignoreWords.every((w) => !v[0].includes(w) && !v[1].includes(w)), 56 | ); 57 | } catch (error) { 58 | console.error("[Fetch] failed to fetch tw prompts", error); 59 | return []; 60 | } 61 | } 62 | 63 | async function fetchEN() { 64 | console.log("[Fetch] fetching en prompts..."); 65 | try { 66 | // const raw = await (await fetch(EN_URL)).text(); 67 | const response = await Promise.race([fetch(EN_URL), timeoutPromise(5000)]); 68 | const raw = await response.text(); 69 | return raw 70 | .split("\n") 71 | .slice(1) 72 | .map((v) => 73 | v 74 | .split('","') 75 | .map((v) => v.replace(/^"|"$/g, "").replaceAll('""', '"')) 76 | .filter((v) => v[0] && v[1]), 77 | ); 78 | } catch (error) { 79 | console.error("[Fetch] failed to fetch en prompts", error); 80 | return []; 81 | } 82 | } 83 | 84 | async function main() { 85 | Promise.all([fetchCN(), fetchTW(), fetchEN()]) 86 | .then(([cn, tw, en]) => { 87 | fs.writeFile(FILE, JSON.stringify({ cn, tw, en })); 88 | }) 89 | .catch((e) => { 90 | console.error("[Fetch] failed to fetch prompts"); 91 | fs.writeFile(FILE, JSON.stringify({ cn: [], tw: [], en: [] })); 92 | }) 93 | .finally(() => { 94 | console.log("[Fetch] saved to " + FILE); 95 | }); 96 | } 97 | 98 | main(); 99 | -------------------------------------------------------------------------------- /app/api/stability/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getServerSideConfig } from "@/app/config/server"; 3 | import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant"; 4 | import { auth } from "@/app/api/auth"; 5 | 6 | async function handle( 7 | req: NextRequest, 8 | { params }: { params: { path: string[] } }, 9 | ) { 10 | console.log("[Stability] params ", params); 11 | 12 | if (req.method === "OPTIONS") { 13 | return NextResponse.json({ body: "OK" }, { status: 200 }); 14 | } 15 | 16 | const controller = new AbortController(); 17 | 18 | const serverConfig = getServerSideConfig(); 19 | 20 | let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL; 21 | 22 | if (!baseUrl.startsWith("http")) { 23 | baseUrl = `https://${baseUrl}`; 24 | } 25 | 26 | if (baseUrl.endsWith("/")) { 27 | baseUrl = baseUrl.slice(0, -1); 28 | } 29 | 30 | let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", ""); 31 | 32 | console.log("[Stability Proxy] ", path); 33 | console.log("[Stability Base Url]", baseUrl); 34 | 35 | const timeoutId = setTimeout( 36 | () => { 37 | controller.abort(); 38 | }, 39 | 10 * 60 * 1000, 40 | ); 41 | 42 | const authResult = auth(req, ModelProvider.Stability); 43 | 44 | if (authResult.error) { 45 | return NextResponse.json(authResult, { 46 | status: 401, 47 | }); 48 | } 49 | 50 | const bearToken = req.headers.get("Authorization") ?? ""; 51 | const token = bearToken.trim().replaceAll("Bearer ", "").trim(); 52 | 53 | const key = token ? token : serverConfig.stabilityApiKey; 54 | 55 | if (!key) { 56 | return NextResponse.json( 57 | { 58 | error: true, 59 | message: `missing STABILITY_API_KEY in server env vars`, 60 | }, 61 | { 62 | status: 401, 63 | }, 64 | ); 65 | } 66 | 67 | const fetchUrl = `${baseUrl}/${path}`; 68 | console.log("[Stability Url] ", fetchUrl); 69 | const fetchOptions: RequestInit = { 70 | headers: { 71 | "Content-Type": req.headers.get("Content-Type") || "multipart/form-data", 72 | Accept: req.headers.get("Accept") || "application/json", 73 | Authorization: `Bearer ${key}`, 74 | }, 75 | method: req.method, 76 | body: req.body, 77 | // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body 78 | redirect: "manual", 79 | // @ts-ignore 80 | duplex: "half", 81 | signal: controller.signal, 82 | }; 83 | 84 | try { 85 | const res = await fetch(fetchUrl, fetchOptions); 86 | // to prevent browser prompt for credentials 87 | const newHeaders = new Headers(res.headers); 88 | newHeaders.delete("www-authenticate"); 89 | // to disable nginx buffering 90 | newHeaders.set("X-Accel-Buffering", "no"); 91 | return new Response(res.body, { 92 | status: res.status, 93 | statusText: res.statusText, 94 | headers: newHeaders, 95 | }); 96 | } finally { 97 | clearTimeout(timeoutId); 98 | } 99 | } 100 | 101 | export const GET = handle; 102 | export const POST = handle; 103 | 104 | export const runtime = "edge"; 105 | -------------------------------------------------------------------------------- /app/locales/index.ts: -------------------------------------------------------------------------------- 1 | import cn from "./cn"; 2 | import en from "./en"; 3 | import pt from "./pt"; 4 | import tw from "./tw"; 5 | import id from "./id"; 6 | import fr from "./fr"; 7 | import es from "./es"; 8 | import it from "./it"; 9 | import tr from "./tr"; 10 | import jp from "./jp"; 11 | import de from "./de"; 12 | import vi from "./vi"; 13 | import ru from "./ru"; 14 | import no from "./no"; 15 | import cs from "./cs"; 16 | import ko from "./ko"; 17 | import ar from "./ar"; 18 | import bn from "./bn"; 19 | import sk from "./sk"; 20 | import { merge } from "../utils/merge"; 21 | 22 | import type { LocaleType } from "./cn"; 23 | export type { LocaleType, PartialLocaleType } from "./cn"; 24 | 25 | const ALL_LANGS = { 26 | cn, 27 | en, 28 | tw, 29 | pt, 30 | jp, 31 | ko, 32 | id, 33 | fr, 34 | es, 35 | it, 36 | tr, 37 | de, 38 | vi, 39 | ru, 40 | cs, 41 | no, 42 | ar, 43 | bn, 44 | sk, 45 | }; 46 | 47 | export type Lang = keyof typeof ALL_LANGS; 48 | 49 | export const AllLangs = Object.keys(ALL_LANGS) as Lang[]; 50 | 51 | export const ALL_LANG_OPTIONS: Record = { 52 | cn: "简体中文", 53 | en: "English", 54 | pt: "Português", 55 | tw: "繁體中文", 56 | jp: "日本語", 57 | ko: "한국어", 58 | id: "Indonesia", 59 | fr: "Français", 60 | es: "Español", 61 | it: "Italiano", 62 | tr: "Türkçe", 63 | de: "Deutsch", 64 | vi: "Tiếng Việt", 65 | ru: "Русский", 66 | cs: "Čeština", 67 | no: "Nynorsk", 68 | ar: "العربية", 69 | bn: "বাংলা", 70 | sk: "Slovensky", 71 | }; 72 | 73 | const LANG_KEY = "lang"; 74 | const DEFAULT_LANG = "en"; 75 | 76 | const fallbackLang = en; 77 | const targetLang = ALL_LANGS[getLang()] as LocaleType; 78 | 79 | // if target lang missing some fields, it will use fallback lang string 80 | merge(fallbackLang, targetLang); 81 | 82 | export default fallbackLang as LocaleType; 83 | 84 | function getItem(key: string) { 85 | try { 86 | return localStorage.getItem(key); 87 | } catch { 88 | return null; 89 | } 90 | } 91 | 92 | function setItem(key: string, value: string) { 93 | try { 94 | localStorage.setItem(key, value); 95 | } catch {} 96 | } 97 | 98 | function getLanguage() { 99 | try { 100 | const locale = new Intl.Locale(navigator.language).maximize(); 101 | const region = locale?.region?.toLowerCase(); 102 | // 1. check region code in ALL_LANGS 103 | if (AllLangs.includes(region as Lang)) { 104 | return region as Lang; 105 | } 106 | // 2. check language code in ALL_LANGS 107 | if (AllLangs.includes(locale.language as Lang)) { 108 | return locale.language as Lang; 109 | } 110 | return DEFAULT_LANG; 111 | } catch { 112 | return DEFAULT_LANG; 113 | } 114 | } 115 | 116 | export function getLang(): Lang { 117 | const savedLang = getItem(LANG_KEY); 118 | 119 | if (AllLangs.includes((savedLang ?? "") as Lang)) { 120 | return savedLang as Lang; 121 | } 122 | 123 | return getLanguage(); 124 | } 125 | 126 | export function changeLang(lang: Lang) { 127 | setItem(LANG_KEY, lang); 128 | location.reload(); 129 | } 130 | 131 | export function getISOLang() { 132 | const isoLangString: Record = { 133 | cn: "zh-Hans", 134 | tw: "zh-Hant", 135 | }; 136 | 137 | const lang = getLang(); 138 | return isoLangString[lang] ?? lang; 139 | } 140 | -------------------------------------------------------------------------------- /app/api/mj/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getServerSideConfig } from "@/app/config/server"; 3 | import { ModelProvider } from "@/app/constant"; 4 | import { auth } from "@/app/api/auth"; 5 | 6 | async function handle( 7 | req: NextRequest, 8 | { params }: { params: { path: string[] } }, 9 | ) { 10 | console.log("[MJ] params ", params); 11 | 12 | if (req.method === "OPTIONS") { 13 | return NextResponse.json({ body: "OK" }, { status: 200 }); 14 | } 15 | 16 | const controller = new AbortController(); 17 | 18 | const serverConfig = getServerSideConfig(); 19 | 20 | let baseUrl = serverConfig.mjpUrl || ""; 21 | 22 | if (!baseUrl) { 23 | return NextResponse.json( 24 | { 25 | error: true, 26 | message: `missing MJ_PROXY_URL in server env vars`, 27 | }, 28 | { 29 | status: 500, 30 | }, 31 | ); 32 | } 33 | 34 | if (!baseUrl.startsWith("http")) { 35 | baseUrl = `https://${baseUrl}`; 36 | } 37 | 38 | if (baseUrl.endsWith("/")) { 39 | baseUrl = baseUrl.slice(0, -1); 40 | } 41 | 42 | let path = `${req.nextUrl.pathname}`.replaceAll("/api/mj/", ""); 43 | 44 | console.log("[MJ Proxy] ", path); 45 | console.log("[MJ Base Url]", baseUrl); 46 | 47 | const timeoutId = setTimeout( 48 | () => { 49 | controller.abort(); 50 | }, 51 | 10 * 60 * 1000, 52 | ); 53 | 54 | const authResult = auth(req, ModelProvider.Mj); 55 | 56 | if (authResult.error) { 57 | return NextResponse.json(authResult, { 58 | status: 401, 59 | }); 60 | } 61 | 62 | const bearToken = req.headers.get("Authorization")?.trim() ?? ""; 63 | const token = bearToken.startsWith("Bearer ") ? bearToken.slice(7).trim() : bearToken; 64 | 65 | const key = serverConfig.mjpApiKey || token; 66 | 67 | if (!key) { 68 | return NextResponse.json( 69 | { 70 | error: true, 71 | message: `missing MJ_PROXY_KEY in server env vars`, 72 | }, 73 | { 74 | status: 401, 75 | }, 76 | ); 77 | } 78 | 79 | const fetchUrl = `${baseUrl}/${path}`; 80 | console.log("[MJ Url] ", fetchUrl); 81 | const headers = { 82 | "Content-Type": "application/json", 83 | Accept: req.headers.get("Accept") || "application/json", 84 | Authorization: `${key}`, 85 | }; 86 | const fetchOptions: RequestInit = { 87 | headers, 88 | method: req.method, 89 | body: req.body, 90 | // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body 91 | redirect: "manual", 92 | // @ts-ignore 93 | duplex: "half", 94 | signal: controller.signal, 95 | }; 96 | 97 | try { 98 | const res = await fetch(fetchUrl, fetchOptions); 99 | // to prevent browser prompt for credentials 100 | const newHeaders = new Headers(res.headers); 101 | newHeaders.delete("www-authenticate"); 102 | // to disable nginx buffering 103 | newHeaders.set("X-Accel-Buffering", "no"); 104 | return new Response(res.body, { 105 | status: res.status, 106 | statusText: res.statusText, 107 | headers: newHeaders, 108 | }); 109 | } finally { 110 | clearTimeout(timeoutId); 111 | } 112 | } 113 | 114 | export const GET = handle; 115 | export const POST = handle; 116 | 117 | export const runtime = "edge"; 118 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json", 3 | "build": { 4 | "beforeBuildCommand": "yarn export", 5 | "beforeDevCommand": "yarn export:dev", 6 | "devPath": "http://localhost:3000", 7 | "distDir": "../out", 8 | "withGlobalTauri": true 9 | }, 10 | "package": { 11 | "productName": "NextChat", 12 | "version": "3.4.1" 13 | }, 14 | "tauri": { 15 | "allowlist": { 16 | "all": false, 17 | "shell": { 18 | "all": false, 19 | "open": true 20 | }, 21 | "dialog": { 22 | "all": true, 23 | "ask": true, 24 | "confirm": true, 25 | "message": true, 26 | "open": true, 27 | "save": true 28 | }, 29 | "clipboard": { 30 | "all": true, 31 | "writeText": true, 32 | "readText": true 33 | }, 34 | "window": { 35 | "all": false, 36 | "close": true, 37 | "hide": true, 38 | "maximize": true, 39 | "minimize": true, 40 | "setIcon": true, 41 | "setIgnoreCursorEvents": true, 42 | "setResizable": true, 43 | "show": true, 44 | "startDragging": true, 45 | "unmaximize": true, 46 | "unminimize": true 47 | }, 48 | "fs": { 49 | "all": true 50 | }, 51 | "notification": { 52 | "all": true 53 | } 54 | }, 55 | "bundle": { 56 | "active": true, 57 | "category": "DeveloperTool", 58 | "copyright": "2023, Zhang Yifei All Rights Reserved.", 59 | "deb": { 60 | "depends": [] 61 | }, 62 | "externalBin": [], 63 | "icon": [ 64 | "icons/32x32.png", 65 | "icons/128x128.png", 66 | "icons/128x128@2x.png", 67 | "icons/icon.icns", 68 | "icons/icon.ico" 69 | ], 70 | "identifier": "com.yida.chatgpt.next.web", 71 | "longDescription": "NextChat is a cross-platform ChatGPT client, including Web/Win/Linux/OSX/PWA.", 72 | "macOS": { 73 | "entitlements": null, 74 | "exceptionDomain": "", 75 | "frameworks": [], 76 | "providerShortName": null, 77 | "signingIdentity": null 78 | }, 79 | "resources": [], 80 | "shortDescription": "NextChat App", 81 | "targets": "all", 82 | "windows": { 83 | "certificateThumbprint": null, 84 | "digestAlgorithm": "sha256", 85 | "timestampUrl": "" 86 | } 87 | }, 88 | "security": { 89 | "csp": null, 90 | "dangerousUseHttpScheme": true 91 | }, 92 | "updater": { 93 | "active": true, 94 | "endpoints": [ 95 | "https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/releases/latest/download/latest.json" 96 | ], 97 | "dialog": false, 98 | "windows": { 99 | "installMode": "passive" 100 | }, 101 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFNDE4MENFM0Y1RTZBOTQKUldTVWFsNC96b0JCM3RqM2NmMnlFTmxIaStRaEJrTHNOU2VqRVlIV1hwVURoWUdVdEc1eDcxVEYK" 102 | }, 103 | "windows": [ 104 | { 105 | "fullscreen": false, 106 | "height": 600, 107 | "resizable": true, 108 | "title": "NextChat", 109 | "width": 960, 110 | "hiddenTitle": true, 111 | "titleBarStyle": "Overlay" 112 | } 113 | ] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/icons/lightning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/plugin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/cancel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/chatgpt.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | --------------------------------------------------------------------------------