├── .eslintignore ├── vercel.json ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── robots.txt ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── serviceWorkerRegister.js ├── serviceWorker.js └── site.webmanifest ├── .eslintrc.json ├── .husky └── pre-commit ├── docs ├── images │ ├── cover.png │ ├── more.png │ ├── settings.png │ ├── enable-actions.jpg │ ├── enable-actions-sync.jpg │ ├── vercel │ │ ├── vercel-create-1.jpg │ │ ├── vercel-create-2.jpg │ │ ├── vercel-create-3.jpg │ │ ├── vercel-env-edit.jpg │ │ └── vercel-redeploy.jpg │ └── icon.svg ├── vercel-cn.md ├── faq-cn.md └── faq-en.md ├── app ├── store │ ├── index.ts │ ├── access.ts │ ├── update.ts │ └── prompt.ts ├── components │ ├── input-range.module.scss │ ├── settings.module.scss │ ├── window.scss │ ├── input-range.tsx │ ├── button.tsx │ ├── button.module.scss │ ├── error.tsx │ ├── chat.module.scss │ ├── markdown.tsx │ ├── chat-list.tsx │ ├── ui-lib.module.scss │ ├── ui-lib.tsx │ ├── home.tsx │ └── home.module.scss ├── api │ ├── openai │ │ ├── typing.ts │ │ └── route.ts │ ├── config │ │ └── route.ts │ ├── common.ts │ └── chat-stream │ │ └── route.ts ├── page.tsx ├── styles │ ├── animation.scss │ ├── highlight.scss │ └── globals.scss ├── constant.ts ├── config │ ├── build.ts │ └── server.ts ├── polyfill.ts ├── icons │ ├── share.svg │ ├── copy.svg │ ├── close.svg │ ├── send-white.svg │ ├── return.svg │ ├── export.svg │ ├── settings.svg │ ├── clear.svg │ ├── menu.svg │ ├── eye.svg │ ├── reload.svg │ ├── add.svg │ ├── edit.svg │ ├── chat.svg │ ├── three-dots.svg │ ├── download.svg │ ├── user.svg │ ├── eye-off.svg │ ├── brain.svg │ ├── github.svg │ ├── max.svg │ ├── min.svg │ ├── chatgpt.svg │ └── bot.svg ├── layout.tsx ├── locales │ ├── index.ts │ ├── tw.ts │ ├── cn.ts │ ├── jp.ts │ ├── en.ts │ ├── it.ts │ ├── es.ts │ └── tr.ts ├── utils.ts └── requests.ts ├── .lintstagedrc.json ├── .prettierrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── 功能建议.md │ ├── feature_request.md │ ├── 反馈问题.md │ └── bug_report.md └── workflows │ ├── sync.yml │ └── docker.yml ├── next.config.js ├── .gitpod.yml ├── .gitignore ├── tsconfig.json ├── Dockerfile ├── package.json ├── scripts ├── fetch-prompts.mjs └── setup.sh ├── middleware.ts ├── LICENSE ├── README.md └── CODE_OF_CONDUCT.md /.eslintignore: -------------------------------------------------------------------------------- 1 | public/serviceWorker.js -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/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 -------------------------------------------------------------------------------- /docs/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/docs/images/cover.png -------------------------------------------------------------------------------- /docs/images/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/docs/images/more.png -------------------------------------------------------------------------------- /docs/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/docs/images/settings.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | User-agent: vitals.vercel-insights.com 4 | Allow: / -------------------------------------------------------------------------------- /app/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./app"; 2 | export * from "./update"; 3 | export * from "./access"; 4 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/images/enable-actions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/docs/images/enable-actions.jpg -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/images/enable-actions-sync.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/docs/images/enable-actions-sync.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/docs/images/vercel/vercel-create-1.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/docs/images/vercel/vercel-create-2.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/docs/images/vercel/vercel-create-3.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-env-edit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/docs/images/vercel/vercel-env-edit.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-redeploy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/ChatGPT-Web2/main/docs/images/vercel/vercel-redeploy.jpg -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [ 3 | "eslint --fix", 4 | "prettier --write" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /app/components/input-range.module.scss: -------------------------------------------------------------------------------- 1 | .input-range { 2 | border: var(--border-in-light); 3 | border-radius: 10px; 4 | padding: 5px 15px 5px 10px; 5 | font-size: 12px; 6 | display: flex; 7 | } 8 | -------------------------------------------------------------------------------- /.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/api/openai/typing.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CreateChatCompletionRequest, 3 | CreateChatCompletionResponse, 4 | } from "openai"; 5 | 6 | export type ChatRequest = CreateChatCompletionRequest; 7 | export type ChatResponse = CreateChatCompletionResponse; 8 | -------------------------------------------------------------------------------- /.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 | 可以说说你的其他考虑。 21 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const nextConfig = { 4 | experimental: { 5 | appDir: true, 6 | }, 7 | webpack(config) { 8 | config.module.rules.push({ 9 | test: /\.svg$/, 10 | use: ["@svgr/webpack"], 11 | }); 12 | 13 | return config; 14 | }, 15 | output: "standalone", 16 | }; 17 | 18 | module.exports = nextConfig; 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/serviceWorkerRegister.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | window.addEventListener('load', function () { 3 | navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) { 4 | console.log('ServiceWorker registration successful with scope: ', registration.scope); 5 | }, function (err) { 6 | console.error('ServiceWorker registration failed: ', err); 7 | }); 8 | }); 9 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/serviceWorker.js: -------------------------------------------------------------------------------- 1 | const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache"; 2 | 3 | self.addEventListener("activate", function (event) { 4 | console.log("ServiceWorker activated."); 5 | }); 6 | 7 | self.addEventListener("install", function (event) { 8 | event.waitUntil( 9 | caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) { 10 | return cache.addAll([]); 11 | }), 12 | ); 13 | }); 14 | 15 | self.addEventListener("fetch", (e) => {}); 16 | -------------------------------------------------------------------------------- /.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/constant.ts: -------------------------------------------------------------------------------- 1 | export const OWNER = "Yidadaa"; 2 | export const REPO = "ChatGPT-Next-Web"; 3 | export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; 4 | export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; 5 | export const UPDATE_URL = `${REPO_URL}#keep-updated`; 6 | export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; 7 | export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; 8 | export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; 9 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChatGPT Next Web", 3 | "short_name": "ChatGPT", 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 | } 21 | -------------------------------------------------------------------------------- /app/api/config/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | import { getServerSideConfig } from "../../config/server"; 4 | 5 | const serverConfig = getServerSideConfig(); 6 | 7 | // Danger! Don not write any secret value here! 8 | // 警告!不要在这里写入任何敏感信息! 9 | const DANGER_CONFIG = { 10 | needCode: serverConfig.needCode, 11 | }; 12 | 13 | declare global { 14 | type DangerConfig = typeof DANGER_CONFIG; 15 | } 16 | 17 | export async function POST(req: NextRequest) { 18 | return NextResponse.json({ 19 | needCode: serverConfig.needCode, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /app/components/settings.module.scss: -------------------------------------------------------------------------------- 1 | @import "./window.scss"; 2 | 3 | .settings { 4 | padding: 20px; 5 | overflow: auto; 6 | } 7 | 8 | .settings-title { 9 | font-size: 14px; 10 | font-weight: bolder; 11 | } 12 | 13 | .settings-sub-title { 14 | font-size: 12px; 15 | font-weight: normal; 16 | } 17 | 18 | .avatar { 19 | cursor: pointer; 20 | } 21 | 22 | .password-input-container { 23 | max-width: 50%; 24 | display: flex; 25 | justify-content: flex-end; 26 | 27 | .password-eye { 28 | margin-right: 4px; 29 | } 30 | 31 | .password-input { 32 | min-width: 80%; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.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 | public/prompts.json 40 | 41 | .vscode 42 | .idea -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/反馈问题.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 反馈问题 3 | about: 请告诉我们你遇到的问题 4 | title: "[Bug] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **反馈须知** 11 | > 请在下方中括号内输入 x 来表示你已经知晓相关内容。 12 | - [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答; 13 | - [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。 14 | 15 | **描述问题** 16 | 请在此描述你遇到了什么问题。 17 | 18 | **如何复现** 19 | 请告诉我们你是通过什么操作触发的该问题。 20 | 21 | **截图** 22 | 请在此提供控制台截图、屏幕截图或者服务端的 log 截图。 23 | 24 | **一些必要的信息** 25 | - 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16] 26 | - 浏览器: [比如 chrome, safari] 27 | - 版本: [填写设置页面的版本号] 28 | - 部署方式:[比如 vercel、docker 或者服务器部署] 29 | -------------------------------------------------------------------------------- /app/api/common.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | const OPENAI_URL = "api.openai.com"; 4 | const DEFAULT_PROTOCOL = "https"; 5 | const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL; 6 | const BASE_URL = process.env.BASE_URL ?? OPENAI_URL; 7 | 8 | export async function requestOpenai(req: NextRequest) { 9 | const apiKey = req.headers.get("token"); 10 | const openaiPath = req.headers.get("path"); 11 | 12 | console.log("[Proxy] ", openaiPath); 13 | 14 | return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, { 15 | headers: { 16 | "Content-Type": "application/json", 17 | Authorization: `Bearer ${apiKey}`, 18 | }, 19 | method: req.method, 20 | body: req.body, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /app/config/build.ts: -------------------------------------------------------------------------------- 1 | const COMMIT_ID: string = (() => { 2 | try { 3 | const childProcess = require("child_process"); 4 | return ( 5 | childProcess 6 | // .execSync("git describe --tags --abbrev=0") 7 | .execSync("git rev-parse --short HEAD") 8 | .toString() 9 | .trim() 10 | ); 11 | } catch (e) { 12 | console.error("[Build Config] No git or not from git repo."); 13 | return "unknown"; 14 | } 15 | })(); 16 | 17 | export const getBuildConfig = () => { 18 | if (typeof process === "undefined") { 19 | throw Error( 20 | "[Server Config] you are importing a nodejs-only module outside of nodejs", 21 | ); 22 | } 23 | 24 | return { 25 | commitId: COMMIT_ID, 26 | }; 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 | -------------------------------------------------------------------------------- /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/icons/share.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/components/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 | margin-top: 5px; 28 | } 29 | } 30 | 31 | .window-actions { 32 | display: inline-flex; 33 | } 34 | 35 | .window-action-button { 36 | margin-left: 10px; 37 | } 38 | -------------------------------------------------------------------------------- /app/api/openai/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { requestOpenai } from "../common"; 3 | 4 | async function makeRequest(req: NextRequest) { 5 | try { 6 | const api = await requestOpenai(req); 7 | const res = new NextResponse(api.body); 8 | res.headers.set("Content-Type", "application/json"); 9 | res.headers.set("Cache-Control", "no-cache"); 10 | return res; 11 | } catch (e) { 12 | console.error("[OpenAI] ", req.body, e); 13 | return NextResponse.json( 14 | { 15 | error: true, 16 | msg: JSON.stringify(e), 17 | }, 18 | { 19 | status: 500, 20 | }, 21 | ); 22 | } 23 | } 24 | 25 | export async function POST(req: NextRequest) { 26 | return makeRequest(req); 27 | } 28 | 29 | export async function GET(req: NextRequest) { 30 | return makeRequest(req); 31 | } 32 | -------------------------------------------------------------------------------- /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/icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/close.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/icons/send-white.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/components/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import styles from "./button.module.scss"; 4 | 5 | export function IconButton(props: { 6 | onClick?: () => void; 7 | icon: JSX.Element; 8 | text?: string; 9 | bordered?: boolean; 10 | shadow?: boolean; 11 | noDark?: boolean; 12 | className?: string; 13 | title?: string; 14 | disabled?: boolean; 15 | }) { 16 | return ( 17 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Upstream Sync 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | schedule: 8 | - cron: "0 * * * *" # every hour 9 | workflow_dispatch: 10 | 11 | jobs: 12 | sync_latest_from_upstream: 13 | name: Sync latest commits from upstream repo 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event.repository.fork }} 16 | 17 | steps: 18 | # Step 1: run a standard checkout action 19 | - name: Checkout target repo 20 | uses: actions/checkout@v3 21 | 22 | # Step 2: run the sync action 23 | - name: Sync upstream changes 24 | id: sync 25 | uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 26 | with: 27 | upstream_sync_repo: Yidadaa/ChatGPT-Next-Web 28 | upstream_sync_branch: main 29 | target_sync_branch: main 30 | target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set 31 | 32 | # Set test_mode true to run tests instead of the true action!! 33 | test_mode: false 34 | -------------------------------------------------------------------------------- /app/icons/return.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 5 | labels: '' 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 | - [ ] Server 30 | 31 | **Desktop (please complete the following information):** 32 | - OS: [e.g. iOS] 33 | - Browser [e.g. chrome, safari] 34 | - Version [e.g. 22] 35 | 36 | **Smartphone (please complete the following information):** 37 | - Device: [e.g. iPhone6] 38 | - OS: [e.g. iOS8.1] 39 | - Browser [e.g. stock browser, safari] 40 | - Version [e.g. 22] 41 | 42 | **Additional Logs** 43 | Add any logs about the problem here. 44 | -------------------------------------------------------------------------------- /app/config/server.ts: -------------------------------------------------------------------------------- 1 | import md5 from "spark-md5"; 2 | 3 | declare global { 4 | namespace NodeJS { 5 | interface ProcessEnv { 6 | OPENAI_API_KEY?: string; 7 | CODE?: string; 8 | PROXY_URL?: string; 9 | VERCEL?: string; 10 | } 11 | } 12 | } 13 | 14 | const ACCESS_CODES = (function getAccessCodes(): Set { 15 | const code = process.env.CODE; 16 | 17 | try { 18 | const codes = (code?.split(",") ?? []) 19 | .filter((v) => !!v) 20 | .map((v) => md5.hash(v.trim())); 21 | return new Set(codes); 22 | } catch (e) { 23 | return new Set(); 24 | } 25 | })(); 26 | 27 | export const getServerSideConfig = () => { 28 | if (typeof process === "undefined") { 29 | throw Error( 30 | "[Server Config] you are importing a nodejs-only module outside of nodejs", 31 | ); 32 | } 33 | 34 | return { 35 | apiKey: process.env.OPENAI_API_KEY, 36 | code: process.env.CODE, 37 | codes: ACCESS_CODES, 38 | needCode: ACCESS_CODES.size > 0, 39 | proxyUrl: process.env.PROXY_URL, 40 | isVercel: !!process.env.VERCEL, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /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 | 23 | .shadow { 24 | box-shadow: var(--card-shadow); 25 | } 26 | 27 | .border { 28 | border: var(--border-in-light); 29 | } 30 | 31 | .icon-button:hover { 32 | border-color: var(--primary); 33 | } 34 | 35 | .icon-button-icon { 36 | width: 16px; 37 | height: 16px; 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | } 42 | 43 | @media only screen and (max-width: 600px) { 44 | .icon-button { 45 | padding: 16px; 46 | } 47 | } 48 | 49 | .icon-button-text { 50 | margin-left: 5px; 51 | font-size: 12px; 52 | overflow: hidden; 53 | text-overflow: ellipsis; 54 | white-space: nowrap; 55 | } 56 | -------------------------------------------------------------------------------- /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 的环境变量; 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 即可重新部署。 -------------------------------------------------------------------------------- /app/icons/export.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/icons/clear.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/icons/reload.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 18 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/icons/add.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/components/error.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { IconButton } from "./button"; 3 | import GithubIcon from "../icons/github.svg"; 4 | import { ISSUE_URL } from "../constant"; 5 | 6 | interface IErrorBoundaryState { 7 | hasError: boolean; 8 | error: Error | null; 9 | info: React.ErrorInfo | null; 10 | } 11 | 12 | export class ErrorBoundary extends React.Component { 13 | constructor(props: any) { 14 | super(props); 15 | this.state = { hasError: false, error: null, info: null }; 16 | } 17 | 18 | componentDidCatch(error: Error, info: React.ErrorInfo) { 19 | // Update state with error details 20 | this.setState({ hasError: true, error, info }); 21 | } 22 | 23 | render() { 24 | if (this.state.hasError) { 25 | // Render error message 26 | return ( 27 |
28 |

Oops, something went wrong!

29 |
30 |             {this.state.error?.toString()}
31 |             {this.state.info?.componentStack}
32 |           
33 | 34 | 35 | } 38 | bordered 39 | /> 40 | 41 |
42 | ); 43 | } 44 | // if no error occurred, render children 45 | return this.props.children; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.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: yidadaa/chatgpt-next-web 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 | -------------------------------------------------------------------------------- /app/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/chat.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 18 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/icons/three-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 13 | 14 | 18 | 22 | 23 | 24 | 28 | 32 | 33 | -------------------------------------------------------------------------------- /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 { getBuildConfig } from "./config/build"; 6 | 7 | const buildConfig = getBuildConfig(); 8 | 9 | export const metadata = { 10 | title: "ChatGPT Next Web", 11 | description: "Your personal ChatGPT Chat Bot.", 12 | appleWebApp: { 13 | title: "ChatGPT Next Web", 14 | statusBarStyle: "default", 15 | }, 16 | themeColor: "#fafafa", 17 | }; 18 | 19 | export default function RootLayout({ 20 | children, 21 | }: { 22 | children: React.ReactNode; 23 | }) { 24 | return ( 25 | 26 | 27 | 31 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | {children} 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /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.npm.taobao.org' 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 CODE="" 20 | 21 | WORKDIR /app 22 | COPY --from=deps /app/node_modules ./node_modules 23 | COPY . . 24 | 25 | RUN yarn build 26 | 27 | FROM base AS runner 28 | WORKDIR /app 29 | 30 | RUN apk add proxychains-ng 31 | 32 | ENV PROXY_URL="" 33 | ENV OPENAI_API_KEY="" 34 | ENV CODE="" 35 | 36 | COPY --from=builder /app/public ./public 37 | COPY --from=builder /app/.next/standalone ./ 38 | COPY --from=builder /app/.next/static ./.next/static 39 | COPY --from=builder /app/.next/server ./.next/server 40 | 41 | EXPOSE 3000 42 | 43 | CMD if [ -n "$PROXY_URL" ]; then \ 44 | protocol=$(echo $PROXY_URL | cut -d: -f1); \ 45 | host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ 46 | port=$(echo $PROXY_URL | cut -d: -f3); \ 47 | conf=/etc/proxychains.conf; \ 48 | echo "strict_chain" > $conf; \ 49 | echo "proxy_dns" >> $conf; \ 50 | echo "remote_dns_subnet 224" >> $conf; \ 51 | echo "tcp_read_time_out 15000" >> $conf; \ 52 | echo "tcp_connect_time_out 8000" >> $conf; \ 53 | echo "[ProxyList]" >> $conf; \ 54 | echo "$protocol $host $port" >> $conf; \ 55 | cat /etc/proxychains.conf; \ 56 | proxychains -f $conf node server.js; \ 57 | else \ 58 | node server.js; \ 59 | fi 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-next-web", 3 | "version": "1.9.3", 4 | "private": false, 5 | "license": "Anti 996", 6 | "scripts": { 7 | "dev": "yarn fetch && next dev", 8 | "build": "yarn fetch && next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "fetch": "node ./scripts/fetch-prompts.mjs", 12 | "prepare": "husky install" 13 | }, 14 | "dependencies": { 15 | "@hello-pangea/dnd": "^16.2.0", 16 | "@svgr/webpack": "^6.5.1", 17 | "@vercel/analytics": "^0.1.11", 18 | "emoji-picker-react": "^4.4.7", 19 | "eventsource-parser": "^0.1.0", 20 | "fuse.js": "^6.6.2", 21 | "next": "^13.2.3", 22 | "node-fetch": "^3.3.1", 23 | "openai": "^3.2.1", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "react-markdown": "^8.0.5", 27 | "rehype-highlight": "^6.0.0", 28 | "rehype-katex": "^6.0.2", 29 | "remark-breaks": "^3.0.2", 30 | "remark-gfm": "^3.0.1", 31 | "remark-math": "^5.1.1", 32 | "sass": "^1.59.2", 33 | "spark-md5": "^3.0.2", 34 | "use-debounce": "^9.0.3", 35 | "zustand": "^4.3.6" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^18.14.6", 39 | "@types/react": "^18.0.28", 40 | "@types/react-dom": "^18.0.11", 41 | "@types/react-katex": "^3.0.0", 42 | "@types/spark-md5": "^3.0.2", 43 | "cross-env": "^7.0.3", 44 | "eslint": "^8.36.0", 45 | "eslint-config-next": "13.2.3", 46 | "eslint-config-prettier": "^8.8.0", 47 | "eslint-plugin-prettier": "^4.2.1", 48 | "husky": "^8.0.0", 49 | "lint-staged": "^13.2.0", 50 | "prettier": "^2.8.7", 51 | "typescript": "4.9.5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/user.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 🤣 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /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 = "https://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_EN_URL = "f/awesome-chatgpt-prompts/main/prompts.csv"; 10 | const EN_URL = MIRRORF_FILE_URL + RAW_EN_URL; 11 | const FILE = "./public/prompts.json"; 12 | 13 | async function fetchCN() { 14 | console.log("[Fetch] fetching cn prompts..."); 15 | try { 16 | const raw = await (await fetch(CN_URL)).json(); 17 | return raw.map((v) => [v.act, v.prompt]); 18 | } catch (error) { 19 | console.error("[Fetch] failed to fetch cn prompts", error); 20 | return []; 21 | } 22 | } 23 | 24 | async function fetchEN() { 25 | console.log("[Fetch] fetching en prompts..."); 26 | try { 27 | const raw = await (await fetch(EN_URL)).text(); 28 | return raw 29 | .split("\n") 30 | .slice(1) 31 | .map((v) => v.split('","').map((v) => v.replace('"', ""))); 32 | } catch (error) { 33 | console.error("[Fetch] failed to fetch cn prompts", error); 34 | return []; 35 | } 36 | } 37 | 38 | async function main() { 39 | Promise.all([fetchCN(), fetchEN()]) 40 | .then(([cn, en]) => { 41 | fs.writeFile(FILE, JSON.stringify({ cn, en })); 42 | }) 43 | .catch((e) => { 44 | console.error("[Fetch] failed to fetch prompts"); 45 | fs.writeFile(FILE, JSON.stringify({ cn: [], en: [] })); 46 | }) 47 | .finally(() => { 48 | console.log("[Fetch] saved to " + FILE); 49 | }); 50 | } 51 | 52 | main(); 53 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getServerSideConfig } from "./app/config/server"; 3 | import md5 from "spark-md5"; 4 | 5 | export const config = { 6 | matcher: ["/api/openai", "/api/chat-stream"], 7 | }; 8 | 9 | const serverConfig = getServerSideConfig(); 10 | 11 | export function middleware(req: NextRequest) { 12 | const accessCode = req.headers.get("access-code"); 13 | const token = req.headers.get("token"); 14 | const hashedCode = md5.hash(accessCode ?? "").trim(); 15 | 16 | console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]); 17 | console.log("[Auth] got access code:", accessCode); 18 | console.log("[Auth] hashed access code:", hashedCode); 19 | 20 | if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) { 21 | return NextResponse.json( 22 | { 23 | error: true, 24 | needAccessCode: true, 25 | msg: "Please go settings page and fill your access code.", 26 | }, 27 | { 28 | status: 401, 29 | }, 30 | ); 31 | } 32 | 33 | // inject api key 34 | if (!token) { 35 | const apiKey = serverConfig.apiKey; 36 | if (apiKey) { 37 | console.log("[Auth] set system token"); 38 | req.headers.set("token", apiKey); 39 | } else { 40 | return NextResponse.json( 41 | { 42 | error: true, 43 | msg: "Empty Api Key", 44 | }, 45 | { 46 | status: 401, 47 | }, 48 | ); 49 | } 50 | } else { 51 | console.log("[Auth] set user token"); 52 | } 53 | 54 | return NextResponse.next({ 55 | request: { 56 | headers: req.headers, 57 | }, 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /app/locales/index.ts: -------------------------------------------------------------------------------- 1 | import CN from "./cn"; 2 | import EN from "./en"; 3 | import TW from "./tw"; 4 | import ES from "./es"; 5 | import IT from "./it"; 6 | import TR from "./tr"; 7 | import JP from "./jp"; 8 | 9 | export type { LocaleType } from "./cn"; 10 | 11 | export const AllLangs = ["en", "cn", "tw", "es", "it", "tr", "jp"] as const; 12 | type Lang = (typeof AllLangs)[number]; 13 | 14 | const LANG_KEY = "lang"; 15 | 16 | function getItem(key: string) { 17 | try { 18 | return localStorage.getItem(key); 19 | } catch { 20 | return null; 21 | } 22 | } 23 | 24 | function setItem(key: string, value: string) { 25 | try { 26 | localStorage.setItem(key, value); 27 | } catch {} 28 | } 29 | 30 | function getLanguage() { 31 | try { 32 | return navigator.language.toLowerCase(); 33 | } catch { 34 | return "cn"; 35 | } 36 | } 37 | 38 | export function getLang(): Lang { 39 | const savedLang = getItem(LANG_KEY); 40 | 41 | if (AllLangs.includes((savedLang ?? "") as Lang)) { 42 | return savedLang as Lang; 43 | } 44 | 45 | const lang = getLanguage(); 46 | 47 | if (lang.includes("zh") || lang.includes("cn")) { 48 | return "cn"; 49 | } else if (lang.includes("tw")) { 50 | return "tw"; 51 | } else if (lang.includes("es")) { 52 | return "es"; 53 | } else if (lang.includes("it")) { 54 | return "it"; 55 | } else if (lang.includes("tr")) { 56 | return "tr"; 57 | } else if (lang.includes("jp")) { 58 | return "jp"; 59 | } else { 60 | return "en"; 61 | } 62 | } 63 | 64 | export function changeLang(lang: Lang) { 65 | setItem(LANG_KEY, lang); 66 | location.reload(); 67 | } 68 | 69 | export default { en: EN, cn: CN, tw: TW, es: ES, it: IT, tr: TR, jp: JP }[ 70 | getLang() 71 | ]; 72 | -------------------------------------------------------------------------------- /app/components/chat.module.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/animation.scss"; 2 | 3 | .prompt-toast { 4 | position: absolute; 5 | bottom: -50px; 6 | z-index: 999; 7 | display: flex; 8 | justify-content: center; 9 | width: calc(100% - 40px); 10 | 11 | .prompt-toast-inner { 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | font-size: 12px; 16 | background-color: var(--white); 17 | color: var(--black); 18 | 19 | border: var(--border-in-light); 20 | box-shadow: var(--card-shadow); 21 | padding: 10px 20px; 22 | border-radius: 100px; 23 | 24 | animation: slide-in-from-top ease 0.3s; 25 | 26 | .prompt-toast-content { 27 | margin-left: 10px; 28 | } 29 | } 30 | } 31 | 32 | .context-prompt { 33 | .context-prompt-row { 34 | display: flex; 35 | justify-content: center; 36 | width: 100%; 37 | margin-bottom: 10px; 38 | 39 | .context-role { 40 | margin-right: 10px; 41 | } 42 | 43 | .context-content { 44 | flex: 1; 45 | max-width: 100%; 46 | text-align: left; 47 | } 48 | 49 | .context-delete-button { 50 | margin-left: 10px; 51 | } 52 | } 53 | 54 | .context-prompt-button { 55 | flex: 1; 56 | } 57 | } 58 | 59 | .memory-prompt { 60 | margin-top: 20px; 61 | 62 | .memory-prompt-title { 63 | font-size: 12px; 64 | font-weight: bold; 65 | margin-bottom: 10px; 66 | display: flex; 67 | justify-content: space-between; 68 | align-items: center; 69 | 70 | .memory-prompt-action { 71 | display: flex; 72 | align-items: center; 73 | } 74 | } 75 | 76 | .memory-prompt-content { 77 | background-color: var(--gray); 78 | border-radius: 6px; 79 | padding: 10px; 80 | font-size: 12px; 81 | user-select: text; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/icons/eye-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/icons/brain.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/store/access.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | 4 | export interface AccessControlStore { 5 | accessCode: string; 6 | token: string; 7 | 8 | needCode: boolean; 9 | 10 | updateToken: (_: string) => void; 11 | updateCode: (_: string) => void; 12 | enabledAccessControl: () => boolean; 13 | isAuthorized: () => boolean; 14 | fetch: () => void; 15 | } 16 | 17 | export const ACCESS_KEY = "access-control"; 18 | 19 | let fetchState = 0; // 0 not fetch, 1 fetching, 2 done 20 | 21 | export const useAccessStore = create()( 22 | persist( 23 | (set, get) => ({ 24 | token: "", 25 | accessCode: "", 26 | needCode: true, 27 | enabledAccessControl() { 28 | get().fetch(); 29 | 30 | return get().needCode; 31 | }, 32 | updateCode(code: string) { 33 | set((state) => ({ accessCode: code })); 34 | }, 35 | updateToken(token: string) { 36 | set((state) => ({ token })); 37 | }, 38 | isAuthorized() { 39 | // has token or has code or disabled access control 40 | return ( 41 | !!get().token || !!get().accessCode || !get().enabledAccessControl() 42 | ); 43 | }, 44 | fetch() { 45 | if (fetchState > 0) return; 46 | fetchState = 1; 47 | fetch("/api/config", { 48 | method: "post", 49 | body: null, 50 | }) 51 | .then((res) => res.json()) 52 | .then((res: DangerConfig) => { 53 | console.log("[Config] got config from server", res); 54 | set(() => ({ ...res })); 55 | }) 56 | .catch(() => { 57 | console.error("[Config] failed to fetch config"); 58 | }) 59 | .finally(() => { 60 | fetchState = 2; 61 | }); 62 | }, 63 | }), 64 | { 65 | name: ACCESS_KEY, 66 | version: 1, 67 | }, 68 | ), 69 | ); 70 | -------------------------------------------------------------------------------- /app/components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from "react-markdown"; 2 | import "katex/dist/katex.min.css"; 3 | import RemarkMath from "remark-math"; 4 | import RemarkBreaks from "remark-breaks"; 5 | import RehypeKatex from "rehype-katex"; 6 | import RemarkGfm from "remark-gfm"; 7 | import RehypeHighlight from "rehype-highlight"; 8 | import { useRef, useState, RefObject, useEffect } from "react"; 9 | import { copyToClipboard } from "../utils"; 10 | 11 | export function PreCode(props: { children: any }) { 12 | const ref = useRef(null); 13 | 14 | return ( 15 |
16 |        {
19 |           if (ref.current) {
20 |             const code = ref.current.innerText;
21 |             copyToClipboard(code);
22 |           }
23 |         }}
24 |       >
25 |       {props.children}
26 |     
27 | ); 28 | } 29 | 30 | const useLazyLoad = (ref: RefObject): boolean => { 31 | const [isIntersecting, setIntersecting] = useState(false); 32 | 33 | useEffect(() => { 34 | const observer = new IntersectionObserver(([entry]) => { 35 | if (entry.isIntersecting) { 36 | setIntersecting(true); 37 | observer.disconnect(); 38 | } 39 | }); 40 | 41 | if (ref.current) { 42 | observer.observe(ref.current); 43 | } 44 | 45 | return () => { 46 | observer.disconnect(); 47 | }; 48 | }, [ref]); 49 | 50 | return isIntersecting; 51 | }; 52 | 53 | export function Markdown(props: { content: string }) { 54 | return ( 55 | 72 | {props.content} 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /app/store/update.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant"; 4 | 5 | export interface UpdateStore { 6 | lastUpdate: number; 7 | remoteVersion: string; 8 | 9 | version: string; 10 | getLatestVersion: (force: boolean) => Promise; 11 | } 12 | 13 | export const UPDATE_KEY = "chat-update"; 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 | 29 | export const useUpdateStore = create()( 30 | persist( 31 | (set, get) => ({ 32 | lastUpdate: 0, 33 | remoteVersion: "", 34 | 35 | version: "unknown", 36 | 37 | async getLatestVersion(force = false) { 38 | set(() => ({ version: queryMeta("version") })); 39 | 40 | const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000; 41 | const shouldFetch = force || overTenMins; 42 | if (!shouldFetch) { 43 | return get().version ?? "unknown"; 44 | } 45 | 46 | try { 47 | // const data = await (await fetch(FETCH_TAG_URL)).json(); 48 | // const remoteId = data[0].name as string; 49 | const data = await (await fetch(FETCH_COMMIT_URL)).json(); 50 | const remoteId = (data[0].sha as string).substring(0, 7); 51 | set(() => ({ 52 | lastUpdate: Date.now(), 53 | remoteVersion: remoteId, 54 | })); 55 | console.log("[Got Upstream] ", remoteId); 56 | return remoteId; 57 | } catch (error) { 58 | console.error("[Fetch Upstream Commit Id]", error); 59 | return get().version ?? ""; 60 | } 61 | }, 62 | }), 63 | { 64 | name: UPDATE_KEY, 65 | version: 1, 66 | }, 67 | ), 68 | ); 69 | -------------------------------------------------------------------------------- /app/icons/github.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 23 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/api/chat-stream/route.ts: -------------------------------------------------------------------------------- 1 | import { createParser } from "eventsource-parser"; 2 | import { NextRequest } from "next/server"; 3 | import { requestOpenai } from "../common"; 4 | 5 | async function createStream(req: NextRequest) { 6 | const encoder = new TextEncoder(); 7 | const decoder = new TextDecoder(); 8 | 9 | const res = await requestOpenai(req); 10 | 11 | const contentType = res.headers.get("Content-Type") ?? ""; 12 | if (!contentType.includes("stream")) { 13 | const content = await ( 14 | await res.text() 15 | ).replace(/provided:.*. You/, "provided: ***. You"); 16 | console.log("[Stream] error ", content); 17 | return "```json\n" + content + "```"; 18 | } 19 | 20 | const stream = new ReadableStream({ 21 | async start(controller) { 22 | function onParse(event: any) { 23 | if (event.type === "event") { 24 | const data = event.data; 25 | // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream 26 | if (data === "[DONE]") { 27 | controller.close(); 28 | return; 29 | } 30 | try { 31 | const json = JSON.parse(data); 32 | const text = json.choices[0].delta.content; 33 | const queue = encoder.encode(text); 34 | controller.enqueue(queue); 35 | } catch (e) { 36 | controller.error(e); 37 | } 38 | } 39 | } 40 | 41 | const parser = createParser(onParse); 42 | for await (const chunk of res.body as any) { 43 | parser.feed(decoder.decode(chunk, { stream: true })); 44 | } 45 | }, 46 | }); 47 | return stream; 48 | } 49 | 50 | export async function POST(req: NextRequest) { 51 | try { 52 | const stream = await createStream(req); 53 | return new Response(stream); 54 | } catch (error) { 55 | console.error("[Chat Stream]", error); 56 | return new Response( 57 | ["```json\n", JSON.stringify(error, null, " "), "\n```"].join(""), 58 | ); 59 | } 60 | } 61 | 62 | export const config = { 63 | runtime: "edge", 64 | }; 65 | -------------------------------------------------------------------------------- /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\")$ ]]; 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=\"centos\"" ]]; then 36 | sudo yum -y install epel-release 37 | sudo yum -y install nodejs git yarn 38 | elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"arch\"" ]]; then 39 | sudo pacman -Syu -y 40 | sudo pacman -S -y nodejs git yarn 41 | else 42 | echo "Unsupported Linux distribution" 43 | exit 1 44 | fi 45 | ;; 46 | Darwin) 47 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 48 | brew install node git yarn 49 | ;; 50 | esac 51 | fi 52 | 53 | # Clone the repository and install dependencies 54 | git clone https://github.com/Yidadaa/ChatGPT-Next-Web 55 | cd ChatGPT-Next-Web 56 | yarn install 57 | 58 | # Prompt user for environment variables 59 | read -p "Enter OPENAI_API_KEY: " OPENAI_API_KEY 60 | read -p "Enter CODE: " CODE 61 | read -p "Enter PORT: " PORT 62 | 63 | # Build and run the project using the environment variables 64 | OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn build 65 | OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn start 66 | -------------------------------------------------------------------------------- /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/icons/max.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 23 | 27 | 30 | 33 | 36 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/icons/min.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 23 | 27 | 31 | 35 | 39 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import DeleteIcon from "../icons/delete.svg"; 2 | import styles from "./home.module.scss"; 3 | import { 4 | DragDropContext, 5 | Droppable, 6 | Draggable, 7 | OnDragEndResponder, 8 | } from "@hello-pangea/dnd"; 9 | 10 | import { useChatStore } from "../store"; 11 | 12 | import Locale from "../locales"; 13 | import { isMobileScreen } from "../utils"; 14 | 15 | export function ChatItem(props: { 16 | onClick?: () => void; 17 | onDelete?: () => void; 18 | title: string; 19 | count: number; 20 | time: string; 21 | selected: boolean; 22 | id: number; 23 | index: number; 24 | }) { 25 | return ( 26 | 27 | {(provided) => ( 28 |
37 |
{props.title}
38 |
39 |
40 | {Locale.ChatItem.ChatItemCount(props.count)} 41 |
42 |
{props.time}
43 |
44 |
45 | 46 |
47 |
48 | )} 49 |
50 | ); 51 | } 52 | 53 | export function ChatList() { 54 | const [sessions, selectedIndex, selectSession, removeSession, moveSession] = 55 | useChatStore((state) => [ 56 | state.sessions, 57 | state.currentSessionIndex, 58 | state.selectSession, 59 | state.removeSession, 60 | state.moveSession, 61 | ]); 62 | const chatStore = useChatStore(); 63 | 64 | const onDragEnd: OnDragEndResponder = (result) => { 65 | const { destination, source } = result; 66 | if (!destination) { 67 | return; 68 | } 69 | 70 | if ( 71 | destination.droppableId === source.droppableId && 72 | destination.index === source.index 73 | ) { 74 | return; 75 | } 76 | 77 | moveSession(source.index, destination.index); 78 | }; 79 | 80 | return ( 81 | 82 | 83 | {(provided) => ( 84 |
89 | {sessions.map((item, i) => ( 90 | selectSession(i)} 99 | onDelete={() => chatStore.deleteSession(i)} 100 | /> 101 | ))} 102 | {provided.placeholder} 103 |
104 | )} 105 |
106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /app/icons/chatgpt.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 版权所有(c)<2023> 2 | 3 | 反996许可证版本1.0 4 | 5 | 在符合下列条件的情况下, 6 | 特此免费向任何得到本授权作品的副本(包括源代码、文件和/或相关内容,以下统称为“授权作品” 7 | )的个人和法人实体授权:被授权个人或法人实体有权以任何目的处置授权作品,包括但不限于使 8 | 用、复制,修改,衍生利用、散布,发布和再许可: 9 | 10 | 11 | 1. 个人或法人实体必须在许可作品的每个再散布或衍生副本上包含以上版权声明和本许可证,不 12 | 得自行修改。 13 | 2. 个人或法人实体必须严格遵守与个人实际所在地或个人出生地或归化地、或法人实体注册地或 14 | 经营地(以较严格者为准)的司法管辖区所有适用的与劳动和就业相关法律、法规、规则和 15 | 标准。如果该司法管辖区没有此类法律、法规、规章和标准或其法律、法规、规章和标准不可 16 | 执行,则个人或法人实体必须遵守国际劳工标准的核心公约。 17 | 3. 个人或法人不得以任何方式诱导或强迫其全职或兼职员工或其独立承包人以口头或书面形式同 18 | 意直接或间接限制、削弱或放弃其所拥有的,受相关与劳动和就业有关的法律、法规、规则和 19 | 标准保护的权利或补救措施,无论该等书面或口头协议是否被该司法管辖区的法律所承认,该 20 | 等个人或法人实体也不得以任何方法限制其雇员或独立承包人向版权持有人或监督许可证合规 21 | 情况的有关当局报告或投诉上述违反许可证的行为的权利。 22 | 23 | 该授权作品是"按原样"提供,不做任何明示或暗示的保证,包括但不限于对适销性、特定用途适用 24 | 性和非侵权性的保证。在任何情况下,无论是在合同诉讼、侵权诉讼或其他诉讼中,版权持有人均 25 | 不承担因本软件或本软件的使用或其他交易而产生、引起或与之相关的任何索赔、损害或其他责任。 26 | 27 | 28 | ------------------------- ENGLISH ------------------------------ 29 | 30 | 31 | Copyright (c) <2023> 32 | 33 | Anti 996 License Version 1.0 (Draft) 34 | 35 | Permission is hereby granted to any individual or legal entity obtaining a copy 36 | of this licensed work (including the source code, documentation and/or related 37 | items, hereinafter collectively referred to as the "licensed work"), free of 38 | charge, to deal with the licensed work for any purpose, including without 39 | limitation, the rights to use, reproduce, modify, prepare derivative works of, 40 | publish, distribute and sublicense the licensed work, subject to the following 41 | conditions: 42 | 43 | 1. The individual or the legal entity must conspicuously display, without 44 | modification, this License on each redistributed or derivative copy of the 45 | Licensed Work. 46 | 47 | 2. The individual or the legal entity must strictly comply with all applicable 48 | laws, regulations, rules and standards of the jurisdiction relating to 49 | labor and employment where the individual is physically located or where 50 | the individual was born or naturalized; or where the legal entity is 51 | registered or is operating (whichever is stricter). In case that the 52 | jurisdiction has no such laws, regulations, rules and standards or its 53 | laws, regulations, rules and standards are unenforceable, the individual 54 | or the legal entity are required to comply with Core International Labor 55 | Standards. 56 | 57 | 3. The individual or the legal entity shall not induce or force its 58 | employee(s), whether full-time or part-time, or its independent 59 | contractor(s), in any methods, to agree in oral or written form, 60 | to directly or indirectly restrict, weaken or relinquish his or 61 | her rights or remedies under such laws, regulations, rules and 62 | standards relating to labor and employment as mentioned above, 63 | no matter whether such written or oral agreement are enforceable 64 | under the laws of the said jurisdiction, nor shall such individual 65 | or the legal entity limit, in any methods, the rights of its employee(s) 66 | or independent contractor(s) from reporting or complaining to the copyright 67 | holder or relevant authorities monitoring the compliance of the license 68 | about its violation(s) of the said license. 69 | 70 | THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 71 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 72 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT 73 | HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 74 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION 75 | WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK. -------------------------------------------------------------------------------- /app/store/prompt.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | import Fuse from "fuse.js"; 4 | import { getLang } from "../locales"; 5 | 6 | export interface Prompt { 7 | id?: number; 8 | title: string; 9 | content: string; 10 | } 11 | 12 | export interface PromptStore { 13 | latestId: number; 14 | prompts: Map; 15 | 16 | add: (prompt: Prompt) => number; 17 | remove: (id: number) => void; 18 | search: (text: string) => Prompt[]; 19 | } 20 | 21 | export const PROMPT_KEY = "prompt-store"; 22 | 23 | export const SearchService = { 24 | ready: false, 25 | engine: new Fuse([], { keys: ["title"] }), 26 | count: { 27 | builtin: 0, 28 | }, 29 | allBuiltInPrompts: [] as Prompt[], 30 | 31 | init(prompts: Prompt[]) { 32 | if (this.ready) { 33 | return; 34 | } 35 | this.allBuiltInPrompts = prompts; 36 | this.engine.setCollection(prompts); 37 | this.ready = true; 38 | }, 39 | 40 | remove(id: number) { 41 | this.engine.remove((doc) => doc.id === id); 42 | }, 43 | 44 | add(prompt: Prompt) { 45 | this.engine.add(prompt); 46 | }, 47 | 48 | search(text: string) { 49 | const results = this.engine.search(text); 50 | return results.map((v) => v.item); 51 | }, 52 | }; 53 | 54 | export const usePromptStore = create()( 55 | persist( 56 | (set, get) => ({ 57 | latestId: 0, 58 | prompts: new Map(), 59 | 60 | add(prompt) { 61 | const prompts = get().prompts; 62 | prompt.id = get().latestId + 1; 63 | prompts.set(prompt.id, prompt); 64 | 65 | set(() => ({ 66 | latestId: prompt.id!, 67 | prompts: prompts, 68 | })); 69 | 70 | return prompt.id!; 71 | }, 72 | 73 | remove(id) { 74 | const prompts = get().prompts; 75 | prompts.delete(id); 76 | SearchService.remove(id); 77 | 78 | set(() => ({ 79 | prompts, 80 | })); 81 | }, 82 | 83 | search(text) { 84 | if (text.length === 0) { 85 | // return all prompts 86 | const userPrompts = get().prompts?.values?.() ?? []; 87 | return SearchService.allBuiltInPrompts.concat([...userPrompts]); 88 | } 89 | return SearchService.search(text) as Prompt[]; 90 | }, 91 | }), 92 | { 93 | name: PROMPT_KEY, 94 | version: 1, 95 | onRehydrateStorage(state) { 96 | const PROMPT_URL = "./prompts.json"; 97 | 98 | type PromptList = Array<[string, string]>; 99 | 100 | fetch(PROMPT_URL) 101 | .then((res) => res.json()) 102 | .then((res) => { 103 | let fetchPrompts = [res.en, res.cn]; 104 | if (getLang() === "cn") { 105 | fetchPrompts = fetchPrompts.reverse(); 106 | } 107 | const builtinPrompts = fetchPrompts 108 | .map((promptList: PromptList) => { 109 | return promptList.map( 110 | ([title, content]) => 111 | ({ 112 | title, 113 | content, 114 | } as Prompt), 115 | ); 116 | }) 117 | .concat([...(state?.prompts?.values() ?? [])]); 118 | 119 | const allPromptsForSearch = builtinPrompts.reduce( 120 | (pre, cur) => pre.concat(cur), 121 | [], 122 | ); 123 | SearchService.count.builtin = res.en.length + res.cn.length; 124 | SearchService.init(allPromptsForSearch); 125 | }); 126 | }, 127 | }, 128 | ), 129 | ); 130 | -------------------------------------------------------------------------------- /app/utils.ts: -------------------------------------------------------------------------------- 1 | import { EmojiStyle } from "emoji-picker-react"; 2 | import { showToast } from "./components/ui-lib"; 3 | import Locale from "./locales"; 4 | 5 | export function trimTopic(topic: string) { 6 | return topic.replace(/[,。!?”“"、,.!?]*$/, ""); 7 | } 8 | 9 | export async function copyToClipboard(text: string) { 10 | try { 11 | await navigator.clipboard.writeText(text); 12 | showToast(Locale.Copy.Success); 13 | } catch (error) { 14 | const textArea = document.createElement("textarea"); 15 | textArea.value = text; 16 | document.body.appendChild(textArea); 17 | textArea.focus(); 18 | textArea.select(); 19 | try { 20 | document.execCommand("copy"); 21 | showToast(Locale.Copy.Success); 22 | } catch (error) { 23 | showToast(Locale.Copy.Failed); 24 | } 25 | document.body.removeChild(textArea); 26 | } 27 | } 28 | 29 | export function downloadAs(text: string, filename: string) { 30 | const element = document.createElement("a"); 31 | element.setAttribute( 32 | "href", 33 | "data:text/plain;charset=utf-8," + encodeURIComponent(text), 34 | ); 35 | element.setAttribute("download", filename); 36 | 37 | element.style.display = "none"; 38 | document.body.appendChild(element); 39 | 40 | element.click(); 41 | 42 | document.body.removeChild(element); 43 | } 44 | 45 | export function isIOS() { 46 | const userAgent = navigator.userAgent.toLowerCase(); 47 | return /iphone|ipad|ipod/.test(userAgent); 48 | } 49 | 50 | export function isMobileScreen() { 51 | return window.innerWidth <= 600; 52 | } 53 | 54 | export function isFirefox() { 55 | return ( 56 | typeof navigator !== "undefined" && /firefox/i.test(navigator.userAgent) 57 | ); 58 | } 59 | 60 | export function selectOrCopy(el: HTMLElement, content: string) { 61 | const currentSelection = window.getSelection(); 62 | 63 | if (currentSelection?.type === "Range") { 64 | return false; 65 | } 66 | 67 | copyToClipboard(content); 68 | 69 | return true; 70 | } 71 | 72 | export function getEmojiUrl(unified: string, style: EmojiStyle) { 73 | return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`; 74 | } 75 | 76 | function getDomContentWidth(dom: HTMLElement) { 77 | const style = window.getComputedStyle(dom); 78 | const paddingWidth = 79 | parseFloat(style.paddingLeft) + parseFloat(style.paddingRight); 80 | const width = dom.clientWidth - paddingWidth; 81 | return width; 82 | } 83 | 84 | function getOrCreateMeasureDom(id: string, init?: (dom: HTMLElement) => void) { 85 | let dom = document.getElementById(id); 86 | 87 | if (!dom) { 88 | dom = document.createElement("span"); 89 | dom.style.position = "absolute"; 90 | dom.style.wordBreak = "break-word"; 91 | dom.style.fontSize = "14px"; 92 | dom.style.transform = "translateY(-200vh)"; 93 | dom.style.pointerEvents = "none"; 94 | dom.style.opacity = "0"; 95 | dom.id = id; 96 | document.body.appendChild(dom); 97 | init?.(dom); 98 | } 99 | 100 | return dom!; 101 | } 102 | 103 | export function autoGrowTextArea(dom: HTMLTextAreaElement) { 104 | const measureDom = getOrCreateMeasureDom("__measure"); 105 | const singleLineDom = getOrCreateMeasureDom("__single_measure", (dom) => { 106 | dom.innerText = "TEXT_FOR_MEASURE"; 107 | }); 108 | 109 | const width = getDomContentWidth(dom); 110 | measureDom.style.width = width + "px"; 111 | measureDom.innerHTML = dom.value.trim().length > 0 ? dom.value : "1"; 112 | 113 | const lineWrapCount = Math.max(0, dom.value.split("\n").length - 1); 114 | const height = parseFloat(window.getComputedStyle(measureDom).height); 115 | const singleLineHeight = parseFloat( 116 | window.getComputedStyle(singleLineDom).height, 117 | ); 118 | 119 | const rows = Math.round(height / singleLineHeight) + lineWrapCount; 120 | 121 | return rows; 122 | } 123 | -------------------------------------------------------------------------------- /app/components/ui-lib.module.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/animation.scss"; 2 | 3 | .card { 4 | background-color: var(--white); 5 | border-radius: 10px; 6 | box-shadow: var(--card-shadow); 7 | padding: 10px; 8 | } 9 | 10 | .popover { 11 | position: relative; 12 | z-index: 2; 13 | } 14 | 15 | .popover-content { 16 | position: absolute; 17 | animation: slide-in 0.3s ease; 18 | right: 0; 19 | top: calc(100% + 10px); 20 | } 21 | 22 | .popover-mask { 23 | position: fixed; 24 | top: 0; 25 | left: 0; 26 | width: 100vw; 27 | height: 100vh; 28 | } 29 | 30 | .list-item { 31 | display: flex; 32 | justify-content: space-between; 33 | align-items: center; 34 | min-height: 40px; 35 | border-bottom: var(--border-in-light); 36 | padding: 10px 20px; 37 | animation: slide-in ease 0.6s; 38 | } 39 | 40 | .list { 41 | border: var(--border-in-light); 42 | border-radius: 10px; 43 | box-shadow: var(--card-shadow); 44 | margin-bottom: 20px; 45 | animation: slide-in ease 0.3s; 46 | } 47 | 48 | .list .list-item:last-child { 49 | border: 0; 50 | } 51 | 52 | .modal-container { 53 | box-shadow: var(--card-shadow); 54 | background-color: var(--white); 55 | border-radius: 12px; 56 | width: 50vw; 57 | animation: slide-in ease 0.3s; 58 | 59 | --modal-padding: 20px; 60 | 61 | .modal-header { 62 | padding: var(--modal-padding); 63 | display: flex; 64 | align-items: center; 65 | justify-content: space-between; 66 | border-bottom: var(--border-in-light); 67 | 68 | .modal-title { 69 | font-weight: bolder; 70 | font-size: 16px; 71 | } 72 | 73 | .modal-close-btn { 74 | cursor: pointer; 75 | 76 | &:hover { 77 | filter: brightness(1.2); 78 | } 79 | } 80 | } 81 | 82 | .modal-content { 83 | max-height: 40vh; 84 | padding: var(--modal-padding); 85 | overflow: auto; 86 | } 87 | 88 | .modal-footer { 89 | padding: var(--modal-padding); 90 | display: flex; 91 | justify-content: flex-end; 92 | 93 | .modal-actions { 94 | display: flex; 95 | align-items: center; 96 | 97 | .modal-action { 98 | &:not(:last-child) { 99 | margin-right: 20px; 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | .show { 107 | opacity: 1; 108 | transition: all ease 0.3s; 109 | transform: translateY(0); 110 | position: fixed; 111 | left: 0; 112 | bottom: 0; 113 | animation: slide-in ease 0.6s; 114 | z-index: 99999; 115 | } 116 | 117 | .hide { 118 | opacity: 0; 119 | transition: all ease 0.3s; 120 | transform: translateY(20px); 121 | } 122 | 123 | .toast-container { 124 | position: fixed; 125 | bottom: 0; 126 | left: 0; 127 | width: 100vw; 128 | display: flex; 129 | justify-content: center; 130 | pointer-events: none; 131 | 132 | .toast-content { 133 | max-width: 80vw; 134 | word-break: break-all; 135 | font-size: 14px; 136 | background-color: var(--white); 137 | box-shadow: var(--card-shadow); 138 | border: var(--border-in-light); 139 | color: var(--black); 140 | padding: 10px 20px; 141 | border-radius: 50px; 142 | margin-bottom: 20px; 143 | display: flex; 144 | align-items: center; 145 | pointer-events: all; 146 | 147 | .toast-action { 148 | padding-left: 20px; 149 | color: var(--primary); 150 | opacity: 0.8; 151 | border: 0; 152 | background: none; 153 | cursor: pointer; 154 | font-family: inherit; 155 | 156 | &:hover { 157 | opacity: 1; 158 | } 159 | } 160 | } 161 | } 162 | 163 | .input { 164 | border: var(--border-in-light); 165 | border-radius: 10px; 166 | padding: 10px; 167 | font-family: inherit; 168 | background-color: var(--white); 169 | color: var(--black); 170 | resize: none; 171 | min-width: 50px; 172 | } 173 | 174 | @media only screen and (max-width: 600px) { 175 | .modal-container { 176 | width: 90vw; 177 | 178 | .modal-content { 179 | max-height: 50vh; 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /app/icons/bot.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/images/icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 预览 3 | 4 |

ChatGPT Next Web

5 | 6 | 一键免费部署你的私人 ChatGPT 网页应用。 7 | 8 | [演示 Demo](https://chat-gpt-next-web.vercel.app/) 9 | 10 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) 11 | 12 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) 13 | 14 | ![主界面](./docs/images/cover.png) 15 | 16 |
17 | 18 | ## 主要功能 19 | 20 | - 在 1 分钟内使用 Vercel **免费一键部署** 21 | - 精心设计的 UI,响应式设计,支持深色模式 22 | - 极快的首屏加载速度(~100kb) 23 | - 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) 24 | - 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 25 | - 一键导出聊天记录,完整的 Markdown 支持 26 | - 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问 27 | 28 | ## 开始使用 29 | 30 | 1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys); 31 | 2. 点击右侧按钮开始部署: 32 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE; 33 | 3. 部署完毕后,即可开始使用; 34 | 4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。 35 | 36 | ## 保持更新 37 | 38 | 如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。 39 | 推荐你按照下列步骤重新部署: 40 | 41 | - 删除掉原先的仓库; 42 | - 使用页面右上角的 fork 按钮,fork 本项目; 43 | - 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。 44 | 45 | ### 打开自动更新 46 | 当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新: 47 | 48 | ![自动更新](./docs/images/enable-actions.jpg) 49 | 50 | ![启用自动更新](./docs/images/enable-actions-sync.jpg) 51 | 52 | ### 手动更新代码 53 | 54 | 如果你想让手动立即更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步。 55 | 56 | 你可以 star/watch 本项目或者 follow 作者来及时获得新功能更新通知。 57 | 58 | ## 配置页面访问密码 59 | 60 | > 配置密码后,用户需要在设置页手动填写访问码才可以正常聊天,否则会通过消息提示未授权状态。 61 | 62 | > **警告**:请务必将密码的位数设置得足够长,最好 7 位以上,否则[会被爆破](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。 63 | 64 | 本项目提供有限的权限控制功能,请在 Vercel 项目控制面板的环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义密码: 65 | 66 | ``` 67 | code1,code2,code3 68 | ``` 69 | 70 | 增加或修改该环境变量后,请**重新部署**项目使改动生效。 71 | 72 | ## 环境变量 73 | 74 | > 本项目大多数配置项都通过环境变量来设置。 75 | 76 | ### `OPENAI_API_KEY` (必填项) 77 | 78 | OpanAI 密钥,你在 openai 账户页面申请的 api key。 79 | 80 | ### `CODE` (可选) 81 | 82 | 访问密码,可选,可以使用逗号隔开多个密码。 83 | 84 | **警告**:如果不填写此项,则任何人都可以直接使用你部署后的网站,可能会导致你的 token 被急速消耗完毕,建议填写此选项。 85 | 86 | ### `BASE_URL` (可选) 87 | 88 | > Default: `api.openai.com` 89 | 90 | OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填写此选项。 91 | 92 | ### `PROTOCOL` (可选) 93 | 94 | > Default: `https` 95 | 96 | > Values: `http` | `https` 97 | 98 | OpenAI 代理接口协议,如果遇到 ssl 证书问题,请尝试通过此选项设置为 http。 99 | 100 | ## 开发 101 | 102 | > 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。 103 | 104 | 点击下方按钮,开始二次开发: 105 | 106 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) 107 | 108 | 在开始写代码之前,需要在项目根目录新建一个 `.env.local` 文件,里面填入环境变量: 109 | 110 | ``` 111 | OPENAI_API_KEY= 112 | ``` 113 | 114 | ### 本地开发 115 | 116 | 1. 安装 nodejs 和 yarn,具体细节请询问 ChatGPT; 117 | 2. 执行 `yarn install && yarn dev` 即可。 118 | 119 | ## 部署 120 | 121 | ### 容器部署 (推荐) 122 | 123 | > 注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。 124 | 125 | ```shell 126 | docker pull yidadaa/chatgpt-next-web 127 | 128 | docker run -d -p 3000:3000 \ 129 | -e OPENAI_API_KEY="sk-xxxx" \ 130 | -e CODE="页面访问密码" \ 131 | yidadaa/chatgpt-next-web 132 | ``` 133 | 134 | 你也可以指定 proxy: 135 | 136 | ```shell 137 | docker run -d -p 3000:3000 \ 138 | -e OPENAI_API_KEY="sk-xxxx" \ 139 | -e CODE="页面访问密码" \ 140 | -e PROXY_URL="http://localhost:7890" \ 141 | yidadaa/chatgpt-next-web 142 | ``` 143 | 144 | ### 本地部署 145 | 146 | 在控制台运行下方命令: 147 | 148 | ```shell 149 | bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) 150 | ``` 151 | 152 | ## 鸣谢 153 | 154 | 本项目来自:https://github.com/Yidadaa/ChatGPT-Next-Web 155 | 156 | ## 开源协议 157 | 158 | > 反对 996,从我开始。 159 | 160 | [Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN) 161 | -------------------------------------------------------------------------------- /app/locales/tw.ts: -------------------------------------------------------------------------------- 1 | import { SubmitKey } from "../store/app"; 2 | import type { LocaleType } from "./index"; 3 | 4 | const tw: LocaleType = { 5 | WIP: "該功能仍在開發中……", 6 | Error: { 7 | Unauthorized: "目前您的狀態是未授權,請前往設定頁面輸入授權碼。", 8 | }, 9 | ChatItem: { 10 | ChatItemCount: (count: number) => `${count} 條對話`, 11 | }, 12 | Chat: { 13 | SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 條對話`, 14 | Actions: { 15 | ChatList: "查看消息列表", 16 | CompressedHistory: "查看壓縮後的歷史 Prompt", 17 | Export: "匯出聊天紀錄", 18 | Copy: "複製", 19 | Stop: "停止", 20 | Retry: "重試", 21 | }, 22 | Rename: "重命名對話", 23 | Typing: "正在輸入…", 24 | Input: (submitKey: string) => { 25 | var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`; 26 | if (submitKey === String(SubmitKey.Enter)) { 27 | inputHints += ",Shift + Enter 鍵換行"; 28 | } 29 | return inputHints; 30 | }, 31 | Send: "發送", 32 | }, 33 | Export: { 34 | Title: "匯出聊天記錄為 Markdown", 35 | Copy: "複製全部", 36 | Download: "下載檔案", 37 | MessageFromYou: "來自你的訊息", 38 | MessageFromChatGPT: "來自 ChatGPT 的訊息", 39 | }, 40 | Memory: { 41 | Title: "上下文記憶 Prompt", 42 | EmptyContent: "尚未記憶", 43 | Copy: "複製全部", 44 | Send: "發送記憶", 45 | Reset: "重置對話", 46 | ResetConfirm: "重置後將清空當前對話記錄以及歷史記憶,確認重置?", 47 | }, 48 | Home: { 49 | NewChat: "新的對話", 50 | DeleteChat: "確定要刪除選取的對話嗎?", 51 | DeleteToast: "已刪除對話", 52 | Revert: "撤銷", 53 | }, 54 | Settings: { 55 | Title: "設定", 56 | SubTitle: "設定選項", 57 | Actions: { 58 | ClearAll: "清除所有數據", 59 | ResetAll: "重置所有設定", 60 | Close: "關閉", 61 | ConfirmResetAll: { 62 | Confirm: "Are you sure you want to reset all configurations?", 63 | }, 64 | ConfirmClearAll: { 65 | Confirm: "Are you sure you want to reset all chat?", 66 | }, 67 | }, 68 | Lang: { 69 | Name: "Language", 70 | Options: { 71 | cn: "简体中文", 72 | en: "English", 73 | tw: "繁體中文", 74 | es: "Español", 75 | it: "Italiano", 76 | tr: "Türkçe", 77 | jp: "日本語", 78 | }, 79 | }, 80 | Avatar: "大頭貼", 81 | FontSize: { 82 | Title: "字型大小", 83 | SubTitle: "聊天內容的字型大小", 84 | }, 85 | Update: { 86 | Version: (x: string) => `當前版本:${x}`, 87 | IsLatest: "已是最新版本", 88 | CheckUpdate: "檢查更新", 89 | IsChecking: "正在檢查更新...", 90 | FoundUpdate: (x: string) => `發現新版本:${x}`, 91 | GoToUpdate: "前往更新", 92 | }, 93 | SendKey: "發送鍵", 94 | Theme: "主題", 95 | TightBorder: "緊湊邊框", 96 | SendPreviewBubble: "發送預覽氣泡", 97 | Prompt: { 98 | Disable: { 99 | Title: "停用提示詞自動補全", 100 | SubTitle: "在輸入框開頭輸入 / 即可觸發自動補全", 101 | }, 102 | List: "自定義提示詞列表", 103 | ListCount: (builtin: number, custom: number) => 104 | `內置 ${builtin} 條,用戶定義 ${custom} 條`, 105 | Edit: "編輯", 106 | }, 107 | HistoryCount: { 108 | Title: "附帶歷史訊息數", 109 | SubTitle: "每次請求附帶的歷史訊息數", 110 | }, 111 | CompressThreshold: { 112 | Title: "歷史訊息長度壓縮閾值", 113 | SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮", 114 | }, 115 | Token: { 116 | Title: "API Key", 117 | SubTitle: "使用自己的 Key 可規避授權訪問限制", 118 | Placeholder: "OpenAI API Key", 119 | }, 120 | Usage: { 121 | Title: "帳戶餘額", 122 | SubTitle(used: any, total: any) { 123 | return `本月已使用 $${used},订阅总额 $${total}`; 124 | }, 125 | IsChecking: "正在檢查…", 126 | Check: "重新檢查", 127 | NoAccess: "輸入API Key查看餘額", 128 | }, 129 | AccessCode: { 130 | Title: "授權碼", 131 | SubTitle: "現在是未授權訪問狀態", 132 | Placeholder: "請輸入授權碼", 133 | }, 134 | Model: "模型 (model)", 135 | Temperature: { 136 | Title: "隨機性 (temperature)", 137 | SubTitle: "值越大,回復越隨機", 138 | }, 139 | MaxTokens: { 140 | Title: "單次回復限制 (max_tokens)", 141 | SubTitle: "單次交互所用的最大 Token 數", 142 | }, 143 | PresencePenlty: { 144 | Title: "話題新穎度 (presence_penalty)", 145 | SubTitle: "值越大,越有可能擴展到新話題", 146 | }, 147 | }, 148 | Store: { 149 | DefaultTopic: "新的對話", 150 | BotHello: "請問需要我的協助嗎?", 151 | Error: "出錯了,請稍後再嘗試", 152 | Prompt: { 153 | History: (content: string) => 154 | "這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content, 155 | Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」", 156 | Summarize: 157 | "簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 200 字以內", 158 | }, 159 | ConfirmClearAll: "確認清除所有對話、設定數據?", 160 | }, 161 | Copy: { 162 | Success: "已複製到剪貼簿中", 163 | Failed: "複製失敗,請賦予剪貼簿權限", 164 | }, 165 | Context: { 166 | Toast: (x: any) => `已設置 ${x} 條前置上下文`, 167 | Edit: "前置上下文和歷史記憶", 168 | Add: "新增壹條", 169 | }, 170 | }; 171 | 172 | export default tw; 173 | -------------------------------------------------------------------------------- /app/locales/cn.ts: -------------------------------------------------------------------------------- 1 | import { SubmitKey } from "../store/app"; 2 | 3 | const cn = { 4 | WIP: "该功能仍在开发中……", 5 | Error: { 6 | Unauthorized: "现在是未授权状态,请点击左下角设置按钮输入访问密码。", 7 | }, 8 | ChatItem: { 9 | ChatItemCount: (count: number) => `${count} 条对话`, 10 | }, 11 | Chat: { 12 | SubTitle: (count: number) => `与 ChatGPT 的 ${count} 条对话`, 13 | Actions: { 14 | ChatList: "查看消息列表", 15 | CompressedHistory: "查看压缩后的历史 Prompt", 16 | Export: "导出聊天记录", 17 | Copy: "复制", 18 | Stop: "停止", 19 | Retry: "重试", 20 | }, 21 | Rename: "重命名对话", 22 | Typing: "正在输入…", 23 | Input: (submitKey: string) => { 24 | var inputHints = `${submitKey} 发送`; 25 | if (submitKey === String(SubmitKey.Enter)) { 26 | inputHints += ",Shift + Enter 换行"; 27 | } 28 | return inputHints + ",/ 触发补全"; 29 | }, 30 | Send: "发送", 31 | }, 32 | Export: { 33 | Title: "导出聊天记录为 Markdown", 34 | Copy: "全部复制", 35 | Download: "下载文件", 36 | MessageFromYou: "来自你的消息", 37 | MessageFromChatGPT: "来自 ChatGPT 的消息", 38 | }, 39 | Memory: { 40 | Title: "历史记忆", 41 | EmptyContent: "尚未记忆", 42 | Send: "发送记忆", 43 | Copy: "复制记忆", 44 | Reset: "重置对话", 45 | ResetConfirm: "重置后将清空当前对话记录以及历史记忆,确认重置?", 46 | }, 47 | Home: { 48 | NewChat: "新的聊天", 49 | DeleteChat: "确认删除选中的对话?", 50 | DeleteToast: "已删除会话", 51 | Revert: "撤销", 52 | }, 53 | Settings: { 54 | Title: "设置", 55 | SubTitle: "设置选项", 56 | Actions: { 57 | ClearAll: "清除所有数据", 58 | ResetAll: "重置所有选项", 59 | Close: "关闭", 60 | ConfirmResetAll: { 61 | Confirm: "Are you sure you want to reset all configurations?", 62 | }, 63 | ConfirmClearAll: { 64 | Confirm: "Are you sure you want to reset all chat?", 65 | }, 66 | }, 67 | Lang: { 68 | Name: "Language", 69 | Options: { 70 | cn: "简体中文", 71 | en: "English", 72 | tw: "繁體中文", 73 | es: "Español", 74 | it: "Italiano", 75 | tr: "Türkçe", 76 | jp: "日本語", 77 | }, 78 | }, 79 | Avatar: "头像", 80 | FontSize: { 81 | Title: "字体大小", 82 | SubTitle: "聊天内容的字体大小", 83 | }, 84 | 85 | Update: { 86 | Version: (x: string) => `当前版本:${x}`, 87 | IsLatest: "已是最新版本", 88 | CheckUpdate: "检查更新", 89 | IsChecking: "正在检查更新...", 90 | FoundUpdate: (x: string) => `发现新版本:${x}`, 91 | GoToUpdate: "前往更新", 92 | }, 93 | SendKey: "发送键", 94 | Theme: "主题", 95 | TightBorder: "无边框模式", 96 | SendPreviewBubble: "发送预览气泡", 97 | Prompt: { 98 | Disable: { 99 | Title: "禁用提示词自动补全", 100 | SubTitle: "在输入框开头输入 / 即可触发自动补全", 101 | }, 102 | List: "自定义提示词列表", 103 | ListCount: (builtin: number, custom: number) => 104 | `内置 ${builtin} 条,用户定义 ${custom} 条`, 105 | Edit: "编辑", 106 | }, 107 | HistoryCount: { 108 | Title: "附带历史消息数", 109 | SubTitle: "每次请求携带的历史消息数", 110 | }, 111 | CompressThreshold: { 112 | Title: "历史消息长度压缩阈值", 113 | SubTitle: "当未压缩的历史消息超过该值时,将进行压缩", 114 | }, 115 | Token: { 116 | Title: "API Key", 117 | SubTitle: "使用自己的 Key 可绕过密码访问限制", 118 | Placeholder: "OpenAI API Key", 119 | }, 120 | Usage: { 121 | Title: "余额查询", 122 | SubTitle(used: any, total: any) { 123 | return `本月已使用 $${used},订阅总额 $${total}`; 124 | }, 125 | IsChecking: "正在检查…", 126 | Check: "重新检查", 127 | NoAccess: "输入 API Key 或访问密码查看余额", 128 | }, 129 | AccessCode: { 130 | Title: "访问密码", 131 | SubTitle: "已开启加密访问", 132 | Placeholder: "请输入访问密码", 133 | }, 134 | Model: "模型 (model)", 135 | Temperature: { 136 | Title: "随机性 (temperature)", 137 | SubTitle: "值越大,回复越随机,大于 1 的值可能会导致乱码", 138 | }, 139 | MaxTokens: { 140 | Title: "单次回复限制 (max_tokens)", 141 | SubTitle: "单次交互所用的最大 Token 数", 142 | }, 143 | PresencePenlty: { 144 | Title: "话题新鲜度 (presence_penalty)", 145 | SubTitle: "值越大,越有可能扩展到新话题", 146 | }, 147 | }, 148 | Store: { 149 | DefaultTopic: "新的聊天", 150 | BotHello: "有什么可以帮你的吗", 151 | Error: "出错了,稍后重试吧", 152 | Prompt: { 153 | History: (content: string) => 154 | "这是 ai 和用户的历史聊天总结作为前情提要:" + content, 155 | Topic: 156 | "使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”", 157 | Summarize: 158 | "简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 200 字以内", 159 | }, 160 | ConfirmClearAll: "确认清除所有聊天、设置数据?", 161 | }, 162 | Copy: { 163 | Success: "已写入剪切板", 164 | Failed: "复制失败,请赋予剪切板权限", 165 | }, 166 | Context: { 167 | Toast: (x: any) => `已设置 ${x} 条前置上下文`, 168 | Edit: "前置上下文和历史记忆", 169 | Add: "新增一条", 170 | }, 171 | }; 172 | 173 | export type LocaleType = typeof cn; 174 | 175 | export default cn; 176 | -------------------------------------------------------------------------------- /app/components/ui-lib.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./ui-lib.module.scss"; 2 | import LoadingIcon from "../icons/three-dots.svg"; 3 | import CloseIcon from "../icons/close.svg"; 4 | import { createRoot } from "react-dom/client"; 5 | import React from "react"; 6 | 7 | export function Popover(props: { 8 | children: JSX.Element; 9 | content: JSX.Element; 10 | open?: boolean; 11 | onClose?: () => void; 12 | }) { 13 | return ( 14 |
15 | {props.children} 16 | {props.open && ( 17 |
18 |
19 | {props.content} 20 |
21 | )} 22 |
23 | ); 24 | } 25 | 26 | export function Card(props: { children: JSX.Element[]; className?: string }) { 27 | return ( 28 |
{props.children}
29 | ); 30 | } 31 | 32 | export function ListItem(props: { children: JSX.Element[] }) { 33 | if (props.children.length > 2) { 34 | throw Error("Only Support Two Children"); 35 | } 36 | 37 | return
{props.children}
; 38 | } 39 | 40 | export function List(props: { children: JSX.Element[] | JSX.Element }) { 41 | return
{props.children}
; 42 | } 43 | 44 | export function Loading() { 45 | return ( 46 |
55 | 56 |
57 | ); 58 | } 59 | 60 | interface ModalProps { 61 | title: string; 62 | children?: JSX.Element; 63 | actions?: JSX.Element[]; 64 | onClose?: () => void; 65 | } 66 | export function Modal(props: ModalProps) { 67 | return ( 68 |
69 |
70 |
{props.title}
71 | 72 |
73 | 74 |
75 |
76 | 77 |
{props.children}
78 | 79 |
80 |
81 | {props.actions?.map((action, i) => ( 82 |
83 | {action} 84 |
85 | ))} 86 |
87 |
88 |
89 | ); 90 | } 91 | 92 | export function showModal(props: ModalProps) { 93 | const div = document.createElement("div"); 94 | div.className = "modal-mask"; 95 | document.body.appendChild(div); 96 | 97 | const root = createRoot(div); 98 | const closeModal = () => { 99 | props.onClose?.(); 100 | root.unmount(); 101 | div.remove(); 102 | }; 103 | 104 | div.onclick = (e) => { 105 | if (e.target === div) { 106 | closeModal(); 107 | } 108 | }; 109 | 110 | root.render(); 111 | } 112 | 113 | export type ToastProps = { 114 | content: string; 115 | action?: { 116 | text: string; 117 | onClick: () => void; 118 | }; 119 | }; 120 | 121 | export function Toast(props: ToastProps) { 122 | return ( 123 |
124 |
125 | {props.content} 126 | {props.action && ( 127 | 133 | )} 134 |
135 |
136 | ); 137 | } 138 | 139 | export function showToast( 140 | content: string, 141 | action?: ToastProps["action"], 142 | delay = 3000, 143 | ) { 144 | const div = document.createElement("div"); 145 | div.className = styles.show; 146 | document.body.appendChild(div); 147 | 148 | const root = createRoot(div); 149 | const close = () => { 150 | div.classList.add(styles.hide); 151 | 152 | setTimeout(() => { 153 | root.unmount(); 154 | div.remove(); 155 | }, 300); 156 | }; 157 | 158 | setTimeout(() => { 159 | close(); 160 | }, delay); 161 | 162 | root.render(); 163 | } 164 | 165 | export type InputProps = React.HTMLProps & { 166 | autoHeight?: boolean; 167 | rows?: number; 168 | }; 169 | 170 | export function Input(props: InputProps) { 171 | return ( 172 | 176 | ); 177 | } 178 | -------------------------------------------------------------------------------- /app/locales/jp.ts: -------------------------------------------------------------------------------- 1 | import { SubmitKey } from "../store/app"; 2 | 3 | const jp = { 4 | WIP: "この機能は開発中です……", 5 | Error: { 6 | Unauthorized: 7 | "現在は未承認状態です。左下の設定ボタンをクリックし、アクセスパスワードを入力してください。", 8 | }, 9 | ChatItem: { 10 | ChatItemCount: (count: number) => `${count} 通のチャット`, 11 | }, 12 | Chat: { 13 | SubTitle: (count: number) => `ChatGPTとの ${count} 通のチャット`, 14 | Actions: { 15 | ChatList: "メッセージリストを表示", 16 | CompressedHistory: "圧縮された履歴プロンプトを表示", 17 | Export: "チャット履歴をエクスポート", 18 | Copy: "コピー", 19 | Stop: "停止", 20 | Retry: "リトライ", 21 | }, 22 | Rename: "チャットの名前を変更", 23 | Typing: "入力中…", 24 | Input: (submitKey: string) => { 25 | var inputHints = `${submitKey} で送信`; 26 | if (submitKey === String(SubmitKey.Enter)) { 27 | inputHints += ",Shift + Enter で改行"; 28 | } 29 | return inputHints + ",/ で自動補完をトリガー"; 30 | }, 31 | Send: "送信", 32 | }, 33 | Export: { 34 | Title: "チャット履歴をMarkdown形式でエクスポート", 35 | Copy: "すべてコピー", 36 | Download: "ファイルをダウンロード", 37 | MessageFromYou: "あなたからのメッセージ", 38 | MessageFromChatGPT: "ChatGPTからのメッセージ", 39 | }, 40 | Memory: { 41 | Title: "履歴メモリ", 42 | EmptyContent: "まだ記憶されていません", 43 | Send: "メモリを送信", 44 | Copy: "メモリをコピー", 45 | Reset: "チャットをリセット", 46 | ResetConfirm: 47 | "リセット後、現在のチャット履歴と過去のメモリがクリアされます。リセットしてもよろしいですか?", 48 | }, 49 | Home: { 50 | NewChat: "新しいチャット", 51 | DeleteChat: "選択したチャットを削除してもよろしいですか?", 52 | DeleteToast: "チャットが削除されました", 53 | Revert: "元に戻す", 54 | }, 55 | Settings: { 56 | Title: "設定", 57 | SubTitle: "設定オプション", 58 | Actions: { 59 | ClearAll: "すべてのデータをクリア", 60 | ResetAll: "すべてのオプションをリセット", 61 | Close: "閉じる", 62 | ConfirmResetAll: { 63 | Confirm: "すべての設定をリセットしてもよろしいですか?", 64 | }, 65 | ConfirmClearAll: { 66 | Confirm: "すべてのチャットをリセットしてもよろしいですか?", 67 | }, 68 | }, 69 | Lang: { 70 | Name: "Language", 71 | Options: { 72 | cn: "简体中文", 73 | en: "English", 74 | tw: "繁體中文", 75 | es: "Español", 76 | it: "Italiano", 77 | tr: "Türkçe", 78 | jp: "日本語", 79 | }, 80 | }, 81 | Avatar: "アバター", 82 | FontSize: { 83 | Title: "フォントサイズ", 84 | SubTitle: "チャット内容のフォントサイズ", 85 | }, 86 | 87 | Update: { 88 | Version: (x: string) => `現在のバージョン:${x}`, 89 | IsLatest: "最新バージョンです", 90 | CheckUpdate: "アップデートを確認", 91 | IsChecking: "アップデートを確認しています...", 92 | FoundUpdate: (x: string) => `新しいバージョンが見つかりました:${x}`, 93 | GoToUpdate: "更新する", 94 | }, 95 | SendKey: "送信キー", 96 | Theme: "テーマ", 97 | TightBorder: "ボーダーレスモード", 98 | SendPreviewBubble: "プレビューバブルの送信", 99 | Prompt: { 100 | Disable: { 101 | Title: "プロンプトの自動補完を無効にする", 102 | SubTitle: 103 | "入力フィールドの先頭に / を入力すると、自動補完がトリガーされます。", 104 | }, 105 | List: "カスタムプロンプトリスト", 106 | ListCount: (builtin: number, custom: number) => 107 | `組み込み ${builtin} 件、ユーザー定義 ${custom} 件`, 108 | Edit: "編集", 109 | }, 110 | HistoryCount: { 111 | Title: "履歴メッセージ数を添付", 112 | SubTitle: "リクエストごとに添付する履歴メッセージ数", 113 | }, 114 | CompressThreshold: { 115 | Title: "履歴メッセージの長さ圧縮しきい値", 116 | SubTitle: 117 | "圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。", 118 | }, 119 | Token: { 120 | Title: "APIキー", 121 | SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する", 122 | Placeholder: "OpenAI APIキー", 123 | }, 124 | Usage: { 125 | Title: "残高照会", 126 | SubTitle(used: any, total: any) { 127 | return `今月は $${used} を使用しました。総額は $${total} です。`; 128 | }, 129 | IsChecking: "確認中...", 130 | Check: "再確認", 131 | NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示", 132 | }, 133 | AccessCode: { 134 | Title: "アクセスパスワード", 135 | SubTitle: "暗号化アクセスが有効になっています", 136 | Placeholder: "アクセスパスワードを入力してください", 137 | }, 138 | Model: "モデル (model)", 139 | Temperature: { 140 | Title: "ランダム性 (temperature)", 141 | SubTitle: 142 | "値が大きいほど、回答がランダムになります。1以上の値には文字化けが含まれる可能性があります。", 143 | }, 144 | MaxTokens: { 145 | Title: "シングルレスポンス制限 (max_tokens)", 146 | SubTitle: "1回のインタラクションで使用される最大トークン数", 147 | }, 148 | PresencePenlty: { 149 | Title: "トピックの新鮮度 (presence_penalty)", 150 | SubTitle: "値が大きいほど、新しいトピックへの展開が可能になります。", 151 | }, 152 | }, 153 | Store: { 154 | DefaultTopic: "新しいチャット", 155 | BotHello: "何かお手伝いできることはありますか", 156 | Error: "エラーが発生しました。しばらくしてからやり直してください。", 157 | Prompt: { 158 | History: (content: string) => 159 | "これは、AI とユーザの過去のチャットを要約した前提となるストーリーです:" + 160 | content, 161 | Topic: 162 | "4~5文字でこの文章の簡潔な主題を返してください。説明、句読点、感嘆詞、余分なテキストは無しで。もし主題がない場合は、「おしゃべり」を返してください", 163 | Summarize: 164 | "あなたとユーザの会話を簡潔にまとめて、後続のコンテキストプロンプトとして使ってください。200字以内に抑えてください。", 165 | }, 166 | ConfirmClearAll: 167 | "すべてのチャット、設定データをクリアしてもよろしいですか?", 168 | }, 169 | Copy: { 170 | Success: "クリップボードに書き込みました", 171 | Failed: "コピーに失敗しました。クリップボード許可を与えてください。", 172 | }, 173 | Context: { 174 | Toast: (x: any) => `前置コンテキストが ${x} 件設定されました`, 175 | Edit: "前置コンテキストと履歴メモリ", 176 | Add: "新規追加", 177 | }, 178 | }; 179 | 180 | export type LocaleType = typeof jp; 181 | 182 | export default jp; 183 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | flynn.zhang@foxmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /docs/faq-cn.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | ## 如何快速获得帮助? 4 | 1. 询问ChatGPT / Bing / 百度 / Google等。 5 | 2. 询问网友。请提供问题的背景信息和碰到问题的详细描述。高质量的提问容易获得有用的答案。 6 | 7 | # 部署相关问题 8 | 9 | 各种部署方式详细教程参考:https://rptzik3toh.feishu.cn/docx/XtrdduHwXoSCGIxeFLlcEPsdn8b 10 | 11 | ## 为什么 Docker 部署版本一直提示更新 12 | Docker 版本相当于稳定版,latest Docker 总是与 latest release version 一致,目前我们的发版频率是一到两天发一次,所以 Docker 版本会总是落后最新的提交一到两天,这在预期内。 13 | 14 | ## 如何部署在Vercel上 15 | 1. 注册Github账号,fork该项目 16 | 2. 注册Vercel(需手机验证,可以用中国号码),连接你的Github账户 17 | 3. Vercel上新建项目,选择你在Github fork的项目,按需填写环境变量,开始部署。部署之后,你可以在有梯子的条件下,通过vercel提供的域名访问你的项目。 18 | 4. 如果需要在国内无墙访问:在你的域名管理网站,添加一条域名的CNAME记录,指向cname.vercel-dns.com。之后在Vercel上设置你的域名访问。 19 | 20 | ## 如何修改 Vercel 环境变量 21 | - 进入 vercel 的控制台页面; 22 | - 选中你的 chatgpt next web 项目; 23 | - 点击页面头部的 Settings 选项; 24 | - 找到侧边栏的 Environment Variables 选项; 25 | - 修改对应的值即可。 26 | 27 | ## 环境变量CODE是什么?必须设置吗? 28 | 这是你自定义的访问密码,你可以选择: 29 | 1. 不设置,删除该环境变量即可。谨慎:此时任何人可以访问你的项目。 30 | 2. 部署项目时,设置环境变量CODE(支持多个密码逗号分隔)。设置访问密码后,用户需要在设置界面输入访问密码才可以使用。参见[相关说明](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E9%85%8D%E7%BD%AE%E9%A1%B5%E9%9D%A2%E8%AE%BF%E9%97%AE%E5%AF%86%E7%A0%81) 31 | 32 | ## 为什么我部署的版本没有流式响应 33 | > 相关讨论:[#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386) 34 | 35 | 如果你使用 ngnix 反向代理,需要在配置文件中增加下列代码: 36 | ``` 37 | # 不缓存,支持流式输出 38 | proxy_cache off; # 关闭缓存 39 | proxy_buffering off; # 关闭代理缓冲 40 | chunked_transfer_encoding on; # 开启分块传输编码 41 | tcp_nopush on; # 开启TCP NOPUSH选项,禁止Nagle算法 42 | tcp_nodelay on; # 开启TCP NODELAY选项,禁止延迟ACK算法 43 | keepalive_timeout 300; # 设定keep-alive超时时间为65秒 44 | ``` 45 | 46 | 如果你是在 netlify 部署,此问题依然等待解决,请耐心等待。 47 | 48 | ## 我部署好了,但是无法访问 49 | 请检查排除以下问题: 50 | - 服务启动了吗? 51 | - 端口正确映射了吗? 52 | - 防火墙开放端口了吗? 53 | - 到服务器的路由通吗? 54 | - 域名正确解析了吗? 55 | 56 | ## 什么是代理,如何使用? 57 | 由于OpenAI的IP限制,中国和其他一些国家/地区无法直接连接OpenAI API,需要通过代理。你可以使用代理服务器(正向代理),或者已经设置好的OpenAI API反向代理。 58 | - 正向代理例子:科学上网梯子。docker部署的情况下,设置环境变量HTTP_PROXY为你的代理地址(例如:10.10.10.10:8002)。 59 | - 反向代理例子:可以用别人搭建的代理地址,或者通过Cloudflare免费设置。设置项目环境变量BASE_URL为你的代理地址。 60 | 61 | ## 国内服务器可以部署吗? 62 | 可以但需要解决的问题: 63 | - 需要代理才能连接github和openAI等网站; 64 | - 国内服务器要设置域名解析的话需要备案; 65 | - 国内政策限制代理访问外网/ChatGPT相关应用,可能被封。 66 | 67 | 68 | # 使用相关问题 69 | 70 | ## 为什么会一直提示“出错了,稍后重试吧” 71 | 原因可能有很多,请依次排查: 72 | - 请先检查你的代码版本是否为最新版本,更新到最新版本后重试; 73 | - 请检查 api key 是否设置正确,环境变量名称必须为全大写加下划线; 74 | - 请检查 api key 是否可用; 75 | - 如果经历了上述步骤依旧无法确定问题,请在 issue 区提交一个新 issue,并附上 vercel 的 runtime log 或者 docker 运行时的 log。 76 | 77 | ## 为什么 ChatGPT 的回复会乱码 78 | 设置界面 - 模型设置项中,有一项为 `temperature`,如果此值大于 1,那么就有可能造成回复乱码,将其调回 1 以内即可。 79 | 80 | ## 使用时提示“现在是未授权状态,请在设置页输入访问密码”? 81 | 项目通过环境变量CODE设置了访问密码。第一次使用时,需要到设置中,输入访问码才可以使用。 82 | 83 | ## 使用时提示"You exceeded your current quota, ..." 84 | API KEY有问题。余额不足。 85 | 86 | # 网络服务相关问题 87 | ## Cloudflare是什么? 88 | Cloudflare(CF)是一个提供CDN,域名管理,静态页面托管,边缘计算函数部署等的网络服务供应商。常见的用途:购买和/或托管你的域名(解析、动态域名等),给你的服务器套上CDN(可以隐藏ip免被墙),部署网站(CF Pages)。CF免费提供大多数服务。 89 | 90 | ## Vercel是什么? 91 | Vercel 是一个全球化的云平台,旨在帮助开发人员更快地构建和部署现代 Web 应用程序。本项目以及许多Web应用可以一键免费部署在Vercel上。无需懂代码,无需懂linux,无需服务器,无需付费,无需设置OpenAI API代理。缺点是需要绑定域名才可以在国内无墙访问。 92 | 93 | ## 如何获得一个域名? 94 | 1. 自己去域名供应商处注册,国外有Namesilo(支持支付宝), Cloudflare等等,国内有万网等等; 95 | 2. 免费的域名供应商:eu.org(二级域名)等; 96 | 3. 问朋友要一个免费的二级域名。 97 | 98 | ## 如何获得一台服务器 99 | - 国外服务器供应商举例:亚马逊云,谷歌云,Vultr,Bandwagon,Hostdare,等等; 100 | 国外服务器事项:服务器线路影响国内访问速度,推荐CN2 GIA和CN2线路的服务器。若服务器在国内访问困难(丢包严重等),可以尝试套CDN(Cloudflare等供应商)。 101 | - 国内服务器供应商:阿里云,腾讯等; 102 | 国内服务器事项:解析域名需要备案;国内服务器带宽较贵;访问国外网站(Github, openAI等)需要代理。 103 | 104 | ## 什么情况下服务器要备案? 105 | 在中国大陆经营的网站按监管要求需要备案。实际操作中,服务器位于国内且有域名解析的情况下,服务器供应商会执行监管的备案要求,否则会关停服务。通常的规则如下: 106 | |服务器位置|域名供应商|是否需要备案| 107 | |---|---|---| 108 | |国内|国内|是| 109 | |国内|国外|是| 110 | |国外|国外|否| 111 | |国外|国内|通常否| 112 | 113 | 换服务器供应商后需要转备案。 114 | 115 | # OpenAI相关问题 116 | ## 如何注册OpenAI账号? 117 | 去chat.openai.com注册。你需要: 118 | - 一个良好的梯子(OpenAI支持地区原生IP地址) 119 | - 一个支持的邮箱(例如Gmail或者公司/学校邮箱,非Outlook或qq邮箱) 120 | - 接收短信认证的方式(例如SMS-activate网站) 121 | 122 | ## 怎么开通OpenAI API? 怎么查询API余额? 123 | 官网地址(需梯子):https://platform.openai.com/account/usage 124 | 有网友搭建了无需梯子的余额查询代理,请询问网友获取。请鉴别来源是否可靠,以免API Key泄露。 125 | 126 | ## 我新注册的OpenAI账号怎么没有API余额? 127 | (4月6日更新)新注册账号通常会在24小时后显示API余额。当前新注册账号赠送5美元余额。 128 | 129 | ## 如何给OpenAI API充值? 130 | OpenAI只接受指定地区的信用卡(中国信用卡无法使用)。一些途径举例: 131 | 1. Depay虚拟信用卡 132 | 2. 申请国外信用卡 133 | 3. 网上找人代充 134 | 135 | ## 如何使用GPT-4的API访问? 136 | - GPT-4的API访问需要单独申请。到以下地址填写你的信息进入申请队列waitlist(准备好你的OpenAI组织ID):https://openai.com/waitlist/gpt-4-api 137 | 之后等待邮件消息。 138 | - 开通 ChatGPT Plus 不代表有 GPT-4 权限,两者毫无关系。 139 | 140 | ## 如何使用 Azure OpenAI 接口 141 | 请参考:[#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371) 142 | 143 | ## 为什么我的 Token 消耗得这么快? 144 | > 相关讨论:[#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518) 145 | - 如果你有 GPT 4 的权限,并且日常在使用 GPT 4 api,那么由于 GPT 4 价格是 GPT 3.5 的 15 倍左右,你的账单金额会急速膨胀; 146 | - 如果你在使用 GPT 3.5,并且使用频率并不高,仍然发现自己的账单金额在飞快增加,那么请马上按照以下步骤排查: 147 | - 去 openai 官网查看你的 api key 消费记录,如果你的 token 每小时都有消费,并且每次都消耗了上万 token,那你的 key 一定是泄露了,请立即删除重新生成。**不要在乱七八糟的网站上查余额。** 148 | - 如果你的密码设置很短,比如 5 位以内的字母,那么爆破成本是非常低的,建议你搜索一下 docker 的日志记录,确认是否有人大量尝试了密码组合,关键字:got access code 149 | - 通过上述两个方法就可以定位到你的 token 被快速消耗的原因: 150 | - 如果 openai 消费记录异常,但是 docker 日志没有问题,那么说明是 api key 泄露; 151 | - 如果 docker 日志发现大量 got access code 爆破记录,那么就是密码被爆破了。 152 | 153 | ## API是怎么计费的? 154 | OpenAI网站计费说明:https://openai.com/pricing#language-models 155 | OpenAI根据token数收费,1000个token通常可代表750个英文单词,或500个汉字。输入(Prompt)和输出(Completion)分别统计费用。 156 | |模型|用户输入(Prompt)计费|模型输出(Completion)计费|每次交互最大token数| 157 | |----|----|----|----| 158 | |gpt-3.5|$0.002 / 1千tokens|$0.002 / 1千tokens|4096| 159 | |gpt-4|$0.03 / 1千tokens|$0.06 / 1千tokens|8192| 160 | |gpt-4-32K|$0.06 / 1千tokens|$0.12 / 1千tokens|32768| 161 | 162 | ## gpt-3.5-turbo和gpt3.5-turbo-0301(或者gpt3.5-turbo-mmdd)模型有什么区别? 163 | 官方文档说明:https://platform.openai.com/docs/models/gpt-3-5 164 | - gpt-3.5-turbo是最新的模型,会不断得到更新。 165 | - gpt-3.5-turbo-0301是3月1日定格的模型快照,不会变化,预期3个月后被新快照替代。 166 | -------------------------------------------------------------------------------- /app/locales/en.ts: -------------------------------------------------------------------------------- 1 | import { SubmitKey } from "../store/app"; 2 | import type { LocaleType } from "./index"; 3 | 4 | const en: LocaleType = { 5 | WIP: "WIP...", 6 | Error: { 7 | Unauthorized: 8 | "Unauthorized access, please enter access code in settings page.", 9 | }, 10 | ChatItem: { 11 | ChatItemCount: (count: number) => `${count} messages`, 12 | }, 13 | Chat: { 14 | SubTitle: (count: number) => `${count} messages with ChatGPT`, 15 | Actions: { 16 | ChatList: "Go To Chat List", 17 | CompressedHistory: "Compressed History Memory Prompt", 18 | Export: "Export All Messages as Markdown", 19 | Copy: "Copy", 20 | Stop: "Stop", 21 | Retry: "Retry", 22 | }, 23 | Rename: "Rename Chat", 24 | Typing: "Typing…", 25 | Input: (submitKey: string) => { 26 | var inputHints = `${submitKey} to send`; 27 | if (submitKey === String(SubmitKey.Enter)) { 28 | inputHints += ", Shift + Enter to wrap"; 29 | } 30 | return inputHints + ", / to search prompts"; 31 | }, 32 | Send: "Send", 33 | }, 34 | Export: { 35 | Title: "All Messages", 36 | Copy: "Copy All", 37 | Download: "Download", 38 | MessageFromYou: "Message From You", 39 | MessageFromChatGPT: "Message From ChatGPT", 40 | }, 41 | Memory: { 42 | Title: "Memory Prompt", 43 | EmptyContent: "Nothing yet.", 44 | Send: "Send Memory", 45 | Copy: "Copy Memory", 46 | Reset: "Reset Session", 47 | ResetConfirm: 48 | "Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?", 49 | }, 50 | Home: { 51 | NewChat: "New Chat", 52 | DeleteChat: "Confirm to delete the selected conversation?", 53 | DeleteToast: "Chat Deleted", 54 | Revert: "Revert", 55 | }, 56 | Settings: { 57 | Title: "Settings", 58 | SubTitle: "All Settings", 59 | Actions: { 60 | ClearAll: "Clear All Data", 61 | ResetAll: "Reset All Settings", 62 | Close: "Close", 63 | ConfirmResetAll: { 64 | Confirm: "Are you sure you want to reset all configurations?", 65 | }, 66 | ConfirmClearAll: { 67 | Confirm: "Are you sure you want to reset all chat?", 68 | }, 69 | }, 70 | Lang: { 71 | Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` 72 | Options: { 73 | cn: "简体中文", 74 | en: "English", 75 | tw: "繁體中文", 76 | es: "Español", 77 | it: "Italiano", 78 | tr: "Türkçe", 79 | jp: "日本語", 80 | }, 81 | }, 82 | Avatar: "Avatar", 83 | FontSize: { 84 | Title: "Font Size", 85 | SubTitle: "Adjust font size of chat content", 86 | }, 87 | Update: { 88 | Version: (x: string) => `Version: ${x}`, 89 | IsLatest: "Latest version", 90 | CheckUpdate: "Check Update", 91 | IsChecking: "Checking update...", 92 | FoundUpdate: (x: string) => `Found new version: ${x}`, 93 | GoToUpdate: "Update", 94 | }, 95 | SendKey: "Send Key", 96 | Theme: "Theme", 97 | TightBorder: "Tight Border", 98 | SendPreviewBubble: "Send Preview Bubble", 99 | Prompt: { 100 | Disable: { 101 | Title: "Disable auto-completion", 102 | SubTitle: "Input / to trigger auto-completion", 103 | }, 104 | List: "Prompt List", 105 | ListCount: (builtin: number, custom: number) => 106 | `${builtin} built-in, ${custom} user-defined`, 107 | Edit: "Edit", 108 | }, 109 | HistoryCount: { 110 | Title: "Attached Messages Count", 111 | SubTitle: "Number of sent messages attached per request", 112 | }, 113 | CompressThreshold: { 114 | Title: "History Compression Threshold", 115 | SubTitle: 116 | "Will compress if uncompressed messages length exceeds the value", 117 | }, 118 | Token: { 119 | Title: "API Key", 120 | SubTitle: "Use your key to ignore access code limit", 121 | Placeholder: "OpenAI API Key", 122 | }, 123 | Usage: { 124 | Title: "Account Balance", 125 | SubTitle(used: any, total: any) { 126 | return `Used this month $${used}, subscription $${total}`; 127 | }, 128 | IsChecking: "Checking...", 129 | Check: "Check Again", 130 | NoAccess: "Enter API Key to check balance", 131 | }, 132 | AccessCode: { 133 | Title: "Access Code", 134 | SubTitle: "Access control enabled", 135 | Placeholder: "Need Access Code", 136 | }, 137 | Model: "Model", 138 | Temperature: { 139 | Title: "Temperature", 140 | SubTitle: "A larger value makes the more random output", 141 | }, 142 | MaxTokens: { 143 | Title: "Max Tokens", 144 | SubTitle: "Maximum length of input tokens and generated tokens", 145 | }, 146 | PresencePenlty: { 147 | Title: "Presence Penalty", 148 | SubTitle: 149 | "A larger value increases the likelihood to talk about new topics", 150 | }, 151 | }, 152 | Store: { 153 | DefaultTopic: "New Conversation", 154 | BotHello: "Hello! How can I assist you today?", 155 | Error: "Something went wrong, please try again later.", 156 | Prompt: { 157 | History: (content: string) => 158 | "This is a summary of the chat history between the AI and the user as a recap: " + 159 | content, 160 | Topic: 161 | "Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, or additional text. Remove enclosing quotation marks.", 162 | Summarize: 163 | "Summarize our discussion briefly in 200 words or less to use as a prompt for future context.", 164 | }, 165 | ConfirmClearAll: "Confirm to clear all chat and setting data?", 166 | }, 167 | Copy: { 168 | Success: "Copied to clipboard", 169 | Failed: "Copy failed, please grant permission to access clipboard", 170 | }, 171 | Context: { 172 | Toast: (x: any) => `With ${x} contextual prompts`, 173 | Edit: "Contextual and Memory Prompts", 174 | Add: "Add One", 175 | }, 176 | }; 177 | 178 | export default en; 179 | -------------------------------------------------------------------------------- /app/locales/it.ts: -------------------------------------------------------------------------------- 1 | import { SubmitKey } from "../store/app"; 2 | import type { LocaleType } from "./index"; 3 | 4 | const it: LocaleType = { 5 | WIP: "Work in progress...", 6 | Error: { 7 | Unauthorized: 8 | "Accesso non autorizzato, inserire il codice di accesso nella pagina delle impostazioni.", 9 | }, 10 | ChatItem: { 11 | ChatItemCount: (count: number) => `${count} messaggi`, 12 | }, 13 | Chat: { 14 | SubTitle: (count: number) => `${count} messaggi con ChatGPT`, 15 | Actions: { 16 | ChatList: "Vai alla Chat List", 17 | CompressedHistory: "Prompt di memoria della cronologia compressa", 18 | Export: "Esportazione di tutti i messaggi come Markdown", 19 | Copy: "Copia", 20 | Stop: "Stop", 21 | Retry: "Riprova", 22 | }, 23 | Rename: "Rinomina Chat", 24 | Typing: "Typing…", 25 | Input: (submitKey: string) => { 26 | var inputHints = `Scrivi qualcosa e premi ${submitKey} per inviare`; 27 | if (submitKey === String(SubmitKey.Enter)) { 28 | inputHints += ", premi Shift + Enter per andare a capo"; 29 | } 30 | return inputHints; 31 | }, 32 | Send: "Invia", 33 | }, 34 | Export: { 35 | Title: "Tutti i messaggi", 36 | Copy: "Copia tutto", 37 | Download: "Scarica", 38 | MessageFromYou: "Messaggio da te", 39 | MessageFromChatGPT: "Messaggio da ChatGPT", 40 | }, 41 | Memory: { 42 | Title: "Prompt di memoria", 43 | EmptyContent: "Vuoto.", 44 | Copy: "Copia tutto", 45 | Send: "Send Memory", 46 | Reset: "Reset Session", 47 | ResetConfirm: 48 | "Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?", 49 | }, 50 | Home: { 51 | NewChat: "Nuova Chat", 52 | DeleteChat: "Confermare la cancellazione della conversazione selezionata?", 53 | DeleteToast: "Chat Deleted", 54 | Revert: "Revert", 55 | }, 56 | Settings: { 57 | Title: "Impostazioni", 58 | SubTitle: "Tutte le impostazioni", 59 | Actions: { 60 | ClearAll: "Cancella tutti i dati", 61 | ResetAll: "Resetta tutte le impostazioni", 62 | Close: "Chiudi", 63 | ConfirmResetAll: { 64 | Confirm: "Sei sicuro vuoi cancellare tutte le impostazioni?", 65 | }, 66 | ConfirmClearAll: { 67 | Confirm: "Sei sicuro vuoi cancellare tutte le chat?", 68 | }, 69 | }, 70 | Lang: { 71 | Name: "Lingue", 72 | Options: { 73 | cn: "简体中文", 74 | en: "English", 75 | tw: "繁體中文", 76 | es: "Español", 77 | it: "Italiano", 78 | tr: "Türkçe", 79 | jp: "日本語", 80 | }, 81 | }, 82 | Avatar: "Avatar", 83 | FontSize: { 84 | Title: "Dimensione carattere", 85 | SubTitle: "Regolare la dimensione dei caratteri del contenuto della chat", 86 | }, 87 | Update: { 88 | Version: (x: string) => `Versione: ${x}`, 89 | IsLatest: "Ultima versione", 90 | CheckUpdate: "Controlla aggiornamenti", 91 | IsChecking: "Sto controllando gli aggiornamenti...", 92 | FoundUpdate: (x: string) => `Trovata nuova versione: ${x}`, 93 | GoToUpdate: "Aggiorna", 94 | }, 95 | SendKey: "Tasto invia", 96 | Theme: "tema", 97 | TightBorder: "Bordi stretti", 98 | SendPreviewBubble: "Invia l'anteprima della bolla", 99 | Prompt: { 100 | Disable: { 101 | Title: "Disabilita l'auto completamento", 102 | SubTitle: "Input / per attivare il completamento automatico", 103 | }, 104 | List: "Elenco dei suggerimenti", 105 | ListCount: (builtin: number, custom: number) => 106 | `${builtin} built-in, ${custom} user-defined`, 107 | Edit: "Modifica", 108 | }, 109 | HistoryCount: { 110 | Title: "Conteggio dei messaggi allegati", 111 | SubTitle: "Numero di messaggi inviati allegati per richiesta", 112 | }, 113 | CompressThreshold: { 114 | Title: "Soglia di compressione della cronologia", 115 | SubTitle: 116 | "Comprimerà se la lunghezza dei messaggi non compressi supera il valore", 117 | }, 118 | Token: { 119 | Title: "Chiave API", 120 | SubTitle: 121 | "Utilizzare la chiave per ignorare il limite del codice di accesso", 122 | Placeholder: "OpenAI API Key", 123 | }, 124 | Usage: { 125 | Title: "Bilancio Account", 126 | SubTitle(used: any, total: any) { 127 | return `Usato in questo mese $${used}, subscription $${total}`; 128 | }, 129 | IsChecking: "Controllando...", 130 | Check: "Controlla ancora", 131 | NoAccess: "Inserire la chiave API per controllare il saldo", 132 | }, 133 | AccessCode: { 134 | Title: "Codice d'accesso", 135 | SubTitle: "Controllo d'accesso abilitato", 136 | Placeholder: "Inserisci il codice d'accesso", 137 | }, 138 | Model: "Modello GPT", 139 | Temperature: { 140 | Title: "Temperature", 141 | SubTitle: "Un valore maggiore rende l'output più casuale", 142 | }, 143 | MaxTokens: { 144 | Title: "Token massimi", 145 | SubTitle: "Lunghezza massima dei token in ingresso e dei token generati", 146 | }, 147 | PresencePenlty: { 148 | Title: "Penalità di presenza", 149 | SubTitle: 150 | "Un valore maggiore aumenta la probabilità di parlare di nuovi argomenti", 151 | }, 152 | }, 153 | Store: { 154 | DefaultTopic: "Nuova conversazione", 155 | BotHello: "Ciao, come posso aiutarti oggi?", 156 | Error: "Qualcosa è andato storto, riprova più tardi.", 157 | Prompt: { 158 | History: (content: string) => 159 | "Questo è un riassunto della cronologia delle chat tra l'IA e l'utente:" + 160 | content, 161 | Topic: 162 | "Si prega di generare un titolo di quattro o cinque parole che riassuma la nostra conversazione senza alcuna traccia, punteggiatura, virgolette, punti, simboli o testo aggiuntivo. Rimuovere le virgolette", 163 | Summarize: 164 | "Riassumi brevemente la nostra discussione in 200 caratteri o meno per usarla come spunto per una futura conversazione.", 165 | }, 166 | ConfirmClearAll: 167 | "Confermi la cancellazione di tutti i dati della chat e delle impostazioni?", 168 | }, 169 | Copy: { 170 | Success: "Copiato sugli appunti", 171 | Failed: 172 | "Copia fallita, concedere l'autorizzazione all'accesso agli appunti", 173 | }, 174 | Context: { 175 | Toast: (x: any) => `Con ${x} prompts contestuali`, 176 | Edit: "Prompt contestuali e di memoria", 177 | Add: "Aggiungi altro", 178 | }, 179 | }; 180 | 181 | export default it; 182 | -------------------------------------------------------------------------------- /app/locales/es.ts: -------------------------------------------------------------------------------- 1 | import { SubmitKey } from "../store/app"; 2 | import type { LocaleType } from "./index"; 3 | 4 | const es: LocaleType = { 5 | WIP: "En construcción...", 6 | Error: { 7 | Unauthorized: 8 | "Acceso no autorizado, por favor ingrese el código de acceso en la página de configuración.", 9 | }, 10 | ChatItem: { 11 | ChatItemCount: (count: number) => `${count} mensajes`, 12 | }, 13 | Chat: { 14 | SubTitle: (count: number) => `${count} mensajes con ChatGPT`, 15 | Actions: { 16 | ChatList: "Ir a la lista de chats", 17 | CompressedHistory: "Historial de memoria comprimido", 18 | Export: "Exportar todos los mensajes como Markdown", 19 | Copy: "Copiar", 20 | Stop: "Detener", 21 | Retry: "Reintentar", 22 | }, 23 | Rename: "Renombrar chat", 24 | Typing: "Escribiendo...", 25 | Input: (submitKey: string) => { 26 | var inputHints = `Escribe algo y presiona ${submitKey} para enviar`; 27 | if (submitKey === String(SubmitKey.Enter)) { 28 | inputHints += ", presiona Shift + Enter para nueva línea"; 29 | } 30 | return inputHints; 31 | }, 32 | Send: "Enviar", 33 | }, 34 | Export: { 35 | Title: "Todos los mensajes", 36 | Copy: "Copiar todo", 37 | Download: "Descargar", 38 | MessageFromYou: "Mensaje de ti", 39 | MessageFromChatGPT: "Mensaje de ChatGPT", 40 | }, 41 | Memory: { 42 | Title: "Historial de memoria", 43 | EmptyContent: "Aún no hay nada.", 44 | Copy: "Copiar todo", 45 | Send: "Send Memory", 46 | Reset: "Reset Session", 47 | ResetConfirm: 48 | "Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?", 49 | }, 50 | Home: { 51 | NewChat: "Nuevo chat", 52 | DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?", 53 | DeleteToast: "Chat Deleted", 54 | Revert: "Revert", 55 | }, 56 | Settings: { 57 | Title: "Configuración", 58 | SubTitle: "Todas las configuraciones", 59 | Actions: { 60 | ClearAll: "Borrar todos los datos", 61 | ResetAll: "Restablecer todas las configuraciones", 62 | Close: "Cerrar", 63 | ConfirmResetAll: { 64 | Confirm: "Are you sure you want to reset all configurations?", 65 | }, 66 | ConfirmClearAll: { 67 | Confirm: "Are you sure you want to reset all chat?", 68 | }, 69 | }, 70 | Lang: { 71 | Name: "Language", 72 | Options: { 73 | cn: "简体中文", 74 | en: "Inglés", 75 | tw: "繁體中文", 76 | es: "Español", 77 | it: "Italiano", 78 | tr: "Türkçe", 79 | jp: "日本語", 80 | }, 81 | }, 82 | Avatar: "Avatar", 83 | FontSize: { 84 | Title: "Tamaño de fuente", 85 | SubTitle: "Ajustar el tamaño de fuente del contenido del chat", 86 | }, 87 | Update: { 88 | Version: (x: string) => `Versión: ${x}`, 89 | IsLatest: "Última versión", 90 | CheckUpdate: "Buscar actualizaciones", 91 | IsChecking: "Buscando actualizaciones...", 92 | FoundUpdate: (x: string) => `Se encontró una nueva versión: ${x}`, 93 | GoToUpdate: "Actualizar", 94 | }, 95 | SendKey: "Tecla de envío", 96 | Theme: "Tema", 97 | TightBorder: "Borde ajustado", 98 | SendPreviewBubble: "Enviar burbuja de vista previa", 99 | Prompt: { 100 | Disable: { 101 | Title: "Desactivar autocompletado", 102 | SubTitle: "Escribe / para activar el autocompletado", 103 | }, 104 | List: "Lista de autocompletado", 105 | ListCount: (builtin: number, custom: number) => 106 | `${builtin} incorporado, ${custom} definido por el usuario`, 107 | Edit: "Editar", 108 | }, 109 | HistoryCount: { 110 | Title: "Cantidad de mensajes adjuntos", 111 | SubTitle: "Número de mensajes enviados adjuntos por solicitud", 112 | }, 113 | CompressThreshold: { 114 | Title: "Umbral de compresión de historial", 115 | SubTitle: 116 | "Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor", 117 | }, 118 | Token: { 119 | Title: "Clave de API", 120 | SubTitle: "Utiliza tu clave para ignorar el límite de código de acceso", 121 | Placeholder: "Clave de la API de OpenAI", 122 | }, 123 | Usage: { 124 | Title: "Saldo de la cuenta", 125 | SubTitle(used: any, total: any) { 126 | return `Usado $${used}, subscription $${total}`; 127 | }, 128 | IsChecking: "Comprobando...", 129 | Check: "Comprobar de nuevo", 130 | NoAccess: "Introduzca la clave API para comprobar el saldo", 131 | }, 132 | AccessCode: { 133 | Title: "Código de acceso", 134 | SubTitle: "Control de acceso habilitado", 135 | Placeholder: "Necesita código de acceso", 136 | }, 137 | Model: "Modelo", 138 | Temperature: { 139 | Title: "Temperatura", 140 | SubTitle: "Un valor mayor genera una salida más aleatoria", 141 | }, 142 | MaxTokens: { 143 | Title: "Máximo de tokens", 144 | SubTitle: "Longitud máxima de tokens de entrada y tokens generados", 145 | }, 146 | PresencePenlty: { 147 | Title: "Penalización de presencia", 148 | SubTitle: 149 | "Un valor mayor aumenta la probabilidad de hablar sobre nuevos temas", 150 | }, 151 | }, 152 | Store: { 153 | DefaultTopic: "Nueva conversación", 154 | BotHello: "¡Hola! ¿Cómo puedo ayudarte hoy?", 155 | Error: "Algo salió mal, por favor intenta nuevamente más tarde.", 156 | Prompt: { 157 | History: (content: string) => 158 | "Este es un resumen del historial del chat entre la IA y el usuario como recapitulación: " + 159 | content, 160 | Topic: 161 | "Por favor, genera un título de cuatro a cinco palabras que resuma nuestra conversación sin ningún inicio, puntuación, comillas, puntos, símbolos o texto adicional. Elimina las comillas que lo envuelven.", 162 | Summarize: 163 | "Resuma nuestra discusión brevemente en 200 caracteres o menos para usarlo como un recordatorio para futuros contextos.", 164 | }, 165 | ConfirmClearAll: 166 | "¿Confirmar para borrar todos los datos de chat y configuración?", 167 | }, 168 | Copy: { 169 | Success: "Copiado al portapapeles", 170 | Failed: 171 | "La copia falló, por favor concede permiso para acceder al portapapeles", 172 | }, 173 | Context: { 174 | Toast: (x: any) => `With ${x} contextual prompts`, 175 | Edit: "Contextual and Memory Prompts", 176 | Add: "Add One", 177 | }, 178 | }; 179 | 180 | export default es; 181 | -------------------------------------------------------------------------------- /app/locales/tr.ts: -------------------------------------------------------------------------------- 1 | import { SubmitKey } from "../store/app"; 2 | import type { LocaleType } from "./index"; 3 | 4 | const tr: LocaleType = { 5 | WIP: "Çalışma devam ediyor...", 6 | Error: { 7 | Unauthorized: 8 | "Yetkisiz erişim, lütfen erişim kodunu ayarlar sayfasından giriniz.", 9 | }, 10 | ChatItem: { 11 | ChatItemCount: (count: number) => `${count} mesaj`, 12 | }, 13 | Chat: { 14 | SubTitle: (count: number) => `ChatGPT tarafından ${count} mesaj`, 15 | Actions: { 16 | ChatList: "Sohbet Listesine Git", 17 | CompressedHistory: "Sıkıştırılmış Geçmiş Bellek Komutu", 18 | Export: "Tüm Mesajları Markdown Olarak Dışa Aktar", 19 | Copy: "Kopyala", 20 | Stop: "Durdur", 21 | Retry: "Tekrar Dene", 22 | }, 23 | Rename: "Sohbeti Yeniden Adlandır", 24 | Typing: "Yazıyor…", 25 | Input: (submitKey: string) => { 26 | var inputHints = `Göndermek için ${submitKey}`; 27 | if (submitKey === String(SubmitKey.Enter)) { 28 | inputHints += ", kaydırmak için Shift + Enter"; 29 | } 30 | return inputHints + ", komutları aramak için / (eğik çizgi)"; 31 | }, 32 | Send: "Gönder", 33 | }, 34 | Export: { 35 | Title: "Tüm Mesajlar", 36 | Copy: "Tümünü Kopyala", 37 | Download: "İndir", 38 | MessageFromYou: "Sizin Mesajınız", 39 | MessageFromChatGPT: "ChatGPT'nin Mesajı", 40 | }, 41 | Memory: { 42 | Title: "Bellek Komutları", 43 | EmptyContent: "Henüz değil.", 44 | Send: "Belleği Gönder", 45 | Copy: "Belleği Kopyala", 46 | Reset: "Oturumu Sıfırla", 47 | ResetConfirm: 48 | "Sıfırlama, geçerli görüşme geçmişini ve geçmiş belleği siler. Sıfırlamak istediğinizden emin misiniz?", 49 | }, 50 | Home: { 51 | NewChat: "Yeni Sohbet", 52 | DeleteChat: "Seçili sohbeti silmeyi onaylıyor musunuz?", 53 | DeleteToast: "Sohbet Silindi", 54 | Revert: "Geri Al", 55 | }, 56 | Settings: { 57 | Title: "Ayarlar", 58 | SubTitle: "Tüm Ayarlar", 59 | Actions: { 60 | ClearAll: "Tüm Verileri Temizle", 61 | ResetAll: "Tüm Ayarları Sıfırla", 62 | Close: "Kapat", 63 | ConfirmResetAll: { 64 | Confirm: "Tüm ayarları sıfırlamak istediğinizden emin misiniz?", 65 | }, 66 | ConfirmClearAll: { 67 | Confirm: "Tüm sohbeti sıfırlamak istediğinizden emin misiniz?", 68 | }, 69 | }, 70 | Lang: { 71 | Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` 72 | Options: { 73 | cn: "简体中文", 74 | en: "English", 75 | tw: "繁體中文", 76 | es: "Español", 77 | it: "Italiano", 78 | tr: "Türkçe", 79 | jp: "日本語", 80 | }, 81 | }, 82 | Avatar: "Avatar", 83 | FontSize: { 84 | Title: "Yazı Boyutu", 85 | SubTitle: "Sohbet içeriğinin yazı boyutunu ayarlayın", 86 | }, 87 | Update: { 88 | Version: (x: string) => `Sürüm: ${x}`, 89 | IsLatest: "En son sürüm", 90 | CheckUpdate: "Güncellemeyi Kontrol Et", 91 | IsChecking: "Güncelleme kontrol ediliyor...", 92 | FoundUpdate: (x: string) => `Yeni sürüm bulundu: ${x}`, 93 | GoToUpdate: "Güncelle", 94 | }, 95 | SendKey: "Gönder Tuşu", 96 | Theme: "Tema", 97 | TightBorder: "Tam Ekran", 98 | SendPreviewBubble: "Mesaj Önizleme Balonu", 99 | Prompt: { 100 | Disable: { 101 | Title: "Otomatik tamamlamayı devre dışı bırak", 102 | SubTitle: "Otomatik tamamlamayı kullanmak için / (eğik çizgi) girin", 103 | }, 104 | List: "Komut Listesi", 105 | ListCount: (builtin: number, custom: number) => 106 | `${builtin} yerleşik, ${custom} kullanıcı tanımlı`, 107 | Edit: "Düzenle", 108 | }, 109 | HistoryCount: { 110 | Title: "Ekli Mesaj Sayısı", 111 | SubTitle: "İstek başına ekli gönderilen mesaj sayısı", 112 | }, 113 | CompressThreshold: { 114 | Title: "Geçmiş Sıkıştırma Eşiği", 115 | SubTitle: 116 | "Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır", 117 | }, 118 | Token: { 119 | Title: "API Anahtarı", 120 | SubTitle: "Erişim kodu sınırını yoksaymak için anahtarınızı kullanın", 121 | Placeholder: "OpenAI API Anahtarı", 122 | }, 123 | Usage: { 124 | Title: "Hesap Bakiyesi", 125 | SubTitle(used: any, total: any) { 126 | return `Bu ay kullanılan $${used}, abonelik $${total}`; 127 | }, 128 | IsChecking: "Kontrol ediliyor...", 129 | Check: "Tekrar Kontrol Et", 130 | NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin", 131 | }, 132 | AccessCode: { 133 | Title: "Erişim Kodu", 134 | SubTitle: "Erişim kontrolü etkinleştirme", 135 | Placeholder: "Erişim Kodu Gerekiyor", 136 | }, 137 | Model: "Model", 138 | Temperature: { 139 | Title: "Gerçeklik", 140 | SubTitle: 141 | "Daha büyük bir değer girildiğinde gerçeklik oranı düşer ve daha rastgele çıktılar üretir", 142 | }, 143 | MaxTokens: { 144 | Title: "Maksimum Belirteç", 145 | SubTitle: 146 | "Girdi belirteçlerinin ve oluşturulan belirteçlerin maksimum uzunluğu", 147 | }, 148 | PresencePenlty: { 149 | Title: "Varlık Cezası", 150 | SubTitle: 151 | "Daha büyük bir değer, yeni konular hakkında konuşma olasılığını artırır", 152 | }, 153 | }, 154 | Store: { 155 | DefaultTopic: "Yeni Konuşma", 156 | BotHello: "Merhaba! Size bugün nasıl yardımcı olabilirim?", 157 | Error: "Bir şeyler yanlış gitti. Lütfen daha sonra tekrar deneyiniz.", 158 | Prompt: { 159 | History: (content: string) => 160 | "Bu, yapay zeka ile kullanıcı arasındaki sohbet geçmişinin bir özetidir: " + 161 | content, 162 | Topic: 163 | "Lütfen herhangi bir giriş, noktalama işareti, tırnak işareti, nokta, sembol veya ek metin olmadan konuşmamızı özetleyen dört ila beş kelimelik bir başlık oluşturun. Çevreleyen tırnak işaretlerini kaldırın.", 164 | Summarize: 165 | "Gelecekteki bağlam için bir bilgi istemi olarak kullanmak üzere tartışmamızı en fazla 200 kelimeyle özetleyin.", 166 | }, 167 | ConfirmClearAll: 168 | "Tüm sohbet ve ayar verilerini temizlemeyi onaylıyor musunuz?", 169 | }, 170 | Copy: { 171 | Success: "Panoya kopyalandı", 172 | Failed: "Kopyalama başarısız oldu, lütfen panoya erişim izni verin", 173 | }, 174 | Context: { 175 | Toast: (x: any) => `${x} bağlamsal bellek komutu`, 176 | Edit: "Bağlamsal ve Bellek Komutları", 177 | Add: "Yeni Ekle", 178 | }, 179 | }; 180 | 181 | export default tr; 182 | -------------------------------------------------------------------------------- /app/styles/globals.scss: -------------------------------------------------------------------------------- 1 | @mixin light { 2 | /* color */ 3 | --white: white; 4 | --black: rgb(48, 48, 48); 5 | --gray: rgb(250, 250, 250); 6 | --primary: rgb(29, 147, 171); 7 | --second: rgb(231, 248, 255); 8 | --hover-color: #f3f3f3; 9 | --bar-color: rgba(0, 0, 0, 0.1); 10 | --theme-color: var(--gray); 11 | 12 | /* shadow */ 13 | --shadow: 50px 50px 100px 10px rgb(0, 0, 0, 0.1); 14 | --card-shadow: 0px 2px 4px 0px rgb(0, 0, 0, 0.05); 15 | 16 | /* stroke */ 17 | --border-in-light: 1px solid rgb(222, 222, 222); 18 | } 19 | 20 | @mixin dark { 21 | /* color */ 22 | --white: rgb(30, 30, 30); 23 | --black: rgb(187, 187, 187); 24 | --gray: rgb(21, 21, 21); 25 | --primary: rgb(29, 147, 171); 26 | --second: rgb(27 38 42); 27 | --hover-color: #323232; 28 | 29 | --bar-color: rgba(255, 255, 255, 0.1); 30 | 31 | --border-in-light: 1px solid rgba(255, 255, 255, 0.192); 32 | 33 | --theme-color: var(--gray); 34 | } 35 | 36 | .light { 37 | @include light; 38 | } 39 | 40 | .dark { 41 | @include dark; 42 | } 43 | 44 | .mask { 45 | filter: invert(0.8); 46 | } 47 | 48 | :root { 49 | @include light; 50 | 51 | --window-width: 90vw; 52 | --window-height: 90vh; 53 | --sidebar-width: 300px; 54 | --window-content-width: calc(100% - var(--sidebar-width)); 55 | --message-max-width: 80%; 56 | --full-height: 100%; 57 | } 58 | 59 | @media only screen and (max-width: 600px) { 60 | :root { 61 | --window-width: 100vw; 62 | --window-height: var(--full-height); 63 | --sidebar-width: 100vw; 64 | --window-content-width: var(--window-width); 65 | --message-max-width: 100%; 66 | } 67 | 68 | .no-mobile { 69 | display: none; 70 | } 71 | } 72 | 73 | @media (prefers-color-scheme: dark) { 74 | :root { 75 | @include dark; 76 | } 77 | } 78 | html { 79 | height: var(--full-height); 80 | } 81 | 82 | body { 83 | background-color: var(--gray); 84 | color: var(--black); 85 | margin: 0; 86 | padding: 0; 87 | height: var(--full-height); 88 | width: 100vw; 89 | display: flex; 90 | justify-content: center; 91 | align-items: center; 92 | user-select: none; 93 | font-family: "Noto Sans SC", "SF Pro SC", "SF Pro Text", "SF Pro Icons", 94 | "PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif; 95 | 96 | @media only screen and (max-width: 600px) { 97 | background-color: var(--second); 98 | } 99 | } 100 | 101 | ::-webkit-scrollbar { 102 | --bar-width: 5px; 103 | width: var(--bar-width); 104 | height: var(--bar-width); 105 | } 106 | 107 | ::-webkit-scrollbar-track { 108 | background-color: transparent; 109 | } 110 | 111 | ::-webkit-scrollbar-thumb { 112 | background-color: var(--bar-color); 113 | border-radius: 20px; 114 | background-clip: content-box; 115 | border: 1px solid transparent; 116 | } 117 | 118 | select { 119 | border: var(--border-in-light); 120 | padding: 10px; 121 | border-radius: 10px; 122 | appearance: none; 123 | cursor: pointer; 124 | background-color: var(--white); 125 | color: var(--black); 126 | text-align: center; 127 | } 128 | 129 | label { 130 | cursor: pointer; 131 | } 132 | 133 | input { 134 | text-align: center; 135 | } 136 | 137 | input[type="checkbox"] { 138 | cursor: pointer; 139 | background-color: var(--white); 140 | color: var(--black); 141 | appearance: none; 142 | border: var(--border-in-light); 143 | border-radius: 5px; 144 | height: 16px; 145 | width: 16px; 146 | display: inline-flex; 147 | align-items: center; 148 | justify-content: center; 149 | } 150 | 151 | input[type="checkbox"]:checked::after { 152 | display: inline-block; 153 | width: 8px; 154 | height: 8px; 155 | background-color: var(--primary); 156 | content: " "; 157 | border-radius: 2px; 158 | } 159 | 160 | input[type="range"] { 161 | appearance: none; 162 | background-color: var(--white); 163 | color: var(--black); 164 | } 165 | 166 | @mixin thumb() { 167 | appearance: none; 168 | height: 8px; 169 | width: 20px; 170 | background-color: var(--primary); 171 | border-radius: 10px; 172 | cursor: pointer; 173 | transition: all ease 0.3s; 174 | margin-left: 5px; 175 | border: none; 176 | } 177 | 178 | input[type="range"]::-webkit-slider-thumb { 179 | @include thumb(); 180 | } 181 | 182 | input[type="range"]::-moz-range-thumb { 183 | @include thumb(); 184 | } 185 | 186 | input[type="range"]::-ms-thumb { 187 | @include thumb(); 188 | } 189 | 190 | @mixin thumbHover() { 191 | transform: scaleY(1.2); 192 | width: 24px; 193 | } 194 | 195 | input[type="range"]::-webkit-slider-thumb:hover { 196 | @include thumbHover(); 197 | } 198 | 199 | input[type="range"]::-moz-range-thumb:hover { 200 | @include thumbHover(); 201 | } 202 | 203 | input[type="range"]::-ms-thumb:hover { 204 | @include thumbHover(); 205 | } 206 | 207 | input[type="number"], 208 | input[type="text"], 209 | input[type="password"] { 210 | appearance: none; 211 | border-radius: 10px; 212 | border: var(--border-in-light); 213 | min-height: 36px; 214 | box-sizing: border-box; 215 | background: var(--white); 216 | color: var(--black); 217 | padding: 0 10px; 218 | max-width: 50%; 219 | } 220 | 221 | div.math { 222 | overflow-x: auto; 223 | } 224 | 225 | .modal-mask { 226 | z-index: 9999; 227 | position: fixed; 228 | top: 0; 229 | left: 0; 230 | height: var(--full-height); 231 | width: 100vw; 232 | background-color: rgba($color: #000000, $alpha: 0.5); 233 | display: flex; 234 | align-items: center; 235 | justify-content: center; 236 | } 237 | 238 | .link { 239 | font-size: 12px; 240 | color: var(--primary); 241 | text-decoration: none; 242 | 243 | &:hover { 244 | text-decoration: underline; 245 | } 246 | } 247 | 248 | pre { 249 | position: relative; 250 | 251 | &:hover .copy-code-button { 252 | pointer-events: all; 253 | transform: translateX(0px); 254 | opacity: 0.5; 255 | } 256 | 257 | .copy-code-button { 258 | position: absolute; 259 | right: 10px; 260 | top: 1em; 261 | cursor: pointer; 262 | padding: 0px 5px; 263 | background-color: var(--black); 264 | color: var(--white); 265 | border: var(--border-in-light); 266 | border-radius: 10px; 267 | transform: translateX(10px); 268 | pointer-events: none; 269 | opacity: 0; 270 | transition: all ease 0.3s; 271 | 272 | &:after { 273 | content: "copy"; 274 | } 275 | 276 | &:hover { 277 | opacity: 1; 278 | } 279 | } 280 | } 281 | 282 | .clickable { 283 | cursor: pointer; 284 | 285 | div:not(.no-dark) > svg { 286 | filter: invert(0.5); 287 | } 288 | 289 | &:hover { 290 | filter: brightness(0.9); 291 | } 292 | } 293 | 294 | .error { 295 | width: 80%; 296 | border-radius: 20px; 297 | border: var(--border-in-light); 298 | box-shadow: var(--card-shadow); 299 | padding: 20px; 300 | overflow: auto; 301 | background-color: var(--white); 302 | color: var(--black); 303 | 304 | pre { 305 | overflow: auto; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /app/requests.ts: -------------------------------------------------------------------------------- 1 | import type { ChatRequest, ChatResponse } from "./api/openai/typing"; 2 | import { Message, ModelConfig, useAccessStore, useChatStore } from "./store"; 3 | import { showToast } from "./components/ui-lib"; 4 | 5 | const TIME_OUT_MS = 30000; 6 | 7 | const makeRequestParam = ( 8 | messages: Message[], 9 | options?: { 10 | filterBot?: boolean; 11 | stream?: boolean; 12 | }, 13 | ): ChatRequest => { 14 | let sendMessages = messages.map((v) => ({ 15 | role: v.role, 16 | content: v.content, 17 | })); 18 | 19 | if (options?.filterBot) { 20 | sendMessages = sendMessages.filter((m) => m.role !== "assistant"); 21 | } 22 | 23 | const modelConfig = { ...useChatStore.getState().config.modelConfig }; 24 | 25 | // @yidadaa: wont send max_tokens, because it is nonsense for Muggles 26 | // @ts-expect-error 27 | delete modelConfig.max_tokens; 28 | 29 | return { 30 | messages: sendMessages, 31 | stream: options?.stream, 32 | ...modelConfig, 33 | }; 34 | }; 35 | 36 | function getHeaders() { 37 | const accessStore = useAccessStore.getState(); 38 | let headers: Record = {}; 39 | 40 | if (accessStore.enabledAccessControl()) { 41 | headers["access-code"] = accessStore.accessCode; 42 | } 43 | 44 | if (accessStore.token && accessStore.token.length > 0) { 45 | headers["token"] = accessStore.token; 46 | } 47 | 48 | return headers; 49 | } 50 | 51 | export function requestOpenaiClient(path: string) { 52 | return (body: any, method = "POST") => 53 | fetch("/api/openai?_vercel_no_cache=1", { 54 | method, 55 | headers: { 56 | "Content-Type": "application/json", 57 | path, 58 | ...getHeaders(), 59 | }, 60 | body: body && JSON.stringify(body), 61 | }); 62 | } 63 | 64 | export async function requestChat(messages: Message[]) { 65 | const req: ChatRequest = makeRequestParam(messages, { filterBot: true }); 66 | 67 | const res = await requestOpenaiClient("v1/chat/completions")(req); 68 | 69 | try { 70 | const response = (await res.json()) as ChatResponse; 71 | return response; 72 | } catch (error) { 73 | console.error("[Request Chat] ", error, res.body); 74 | } 75 | } 76 | 77 | export async function requestUsage() { 78 | const formatDate = (d: Date) => 79 | `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d 80 | .getDate() 81 | .toString() 82 | .padStart(2, "0")}`; 83 | const ONE_DAY = 2 * 24 * 60 * 60 * 1000; 84 | const now = new Date(Date.now() + ONE_DAY); 85 | const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); 86 | const startDate = formatDate(startOfMonth); 87 | const endDate = formatDate(now); 88 | 89 | const [used, subs] = await Promise.all([ 90 | requestOpenaiClient( 91 | `dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`, 92 | )(null, "GET"), 93 | requestOpenaiClient("dashboard/billing/subscription")(null, "GET"), 94 | ]); 95 | 96 | const response = (await used.json()) as { 97 | total_usage?: number; 98 | error?: { 99 | type: string; 100 | message: string; 101 | }; 102 | }; 103 | 104 | const total = (await subs.json()) as { 105 | hard_limit_usd?: number; 106 | }; 107 | 108 | if (response.error && response.error.type) { 109 | showToast(response.error.message); 110 | return; 111 | } 112 | 113 | if (response.total_usage) { 114 | response.total_usage = Math.round(response.total_usage) / 100; 115 | } 116 | 117 | return { 118 | used: response.total_usage, 119 | subscription: total.hard_limit_usd, 120 | }; 121 | } 122 | 123 | export async function requestChatStream( 124 | messages: Message[], 125 | options?: { 126 | filterBot?: boolean; 127 | modelConfig?: ModelConfig; 128 | onMessage: (message: string, done: boolean) => void; 129 | onError: (error: Error, statusCode?: number) => void; 130 | onController?: (controller: AbortController) => void; 131 | }, 132 | ) { 133 | const req = makeRequestParam(messages, { 134 | stream: true, 135 | filterBot: options?.filterBot, 136 | }); 137 | 138 | console.log("[Request] ", req); 139 | 140 | const controller = new AbortController(); 141 | const reqTimeoutId = setTimeout(() => controller.abort(), TIME_OUT_MS); 142 | 143 | try { 144 | const res = await fetch("/api/chat-stream", { 145 | method: "POST", 146 | headers: { 147 | "Content-Type": "application/json", 148 | path: "v1/chat/completions", 149 | ...getHeaders(), 150 | }, 151 | body: JSON.stringify(req), 152 | signal: controller.signal, 153 | }); 154 | clearTimeout(reqTimeoutId); 155 | 156 | let responseText = ""; 157 | 158 | const finish = () => { 159 | options?.onMessage(responseText, true); 160 | controller.abort(); 161 | }; 162 | 163 | if (res.ok) { 164 | const reader = res.body?.getReader(); 165 | const decoder = new TextDecoder(); 166 | 167 | options?.onController?.(controller); 168 | 169 | while (true) { 170 | // handle time out, will stop if no response in 10 secs 171 | const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS); 172 | const content = await reader?.read(); 173 | clearTimeout(resTimeoutId); 174 | 175 | if (!content || !content.value) { 176 | break; 177 | } 178 | 179 | const text = decoder.decode(content.value, { stream: true }); 180 | responseText += text; 181 | 182 | const done = content.done; 183 | options?.onMessage(responseText, false); 184 | 185 | if (done) { 186 | break; 187 | } 188 | } 189 | 190 | finish(); 191 | } else if (res.status === 401) { 192 | console.error("Anauthorized"); 193 | options?.onError(new Error("Anauthorized"), res.status); 194 | } else { 195 | console.error("Stream Error", res.body); 196 | options?.onError(new Error("Stream Error"), res.status); 197 | } 198 | } catch (err) { 199 | console.error("NetWork Error", err); 200 | options?.onError(err as Error); 201 | } 202 | } 203 | 204 | export async function requestWithPrompt(messages: Message[], prompt: string) { 205 | messages = messages.concat([ 206 | { 207 | role: "user", 208 | content: prompt, 209 | date: new Date().toLocaleString(), 210 | }, 211 | ]); 212 | 213 | const res = await requestChat(messages); 214 | 215 | return res?.choices?.at(0)?.message?.content ?? ""; 216 | } 217 | 218 | // To store message streaming controller 219 | export const ControllerPool = { 220 | controllers: {} as Record, 221 | 222 | addController( 223 | sessionIndex: number, 224 | messageId: number, 225 | controller: AbortController, 226 | ) { 227 | const key = this.key(sessionIndex, messageId); 228 | this.controllers[key] = controller; 229 | return key; 230 | }, 231 | 232 | stop(sessionIndex: number, messageId: number) { 233 | const key = this.key(sessionIndex, messageId); 234 | const controller = this.controllers[key]; 235 | controller?.abort(); 236 | }, 237 | 238 | remove(sessionIndex: number, messageId: number) { 239 | const key = this.key(sessionIndex, messageId); 240 | delete this.controllers[key]; 241 | }, 242 | 243 | key(sessionIndex: number, messageIndex: number) { 244 | return `${sessionIndex},${messageIndex}`; 245 | }, 246 | }; 247 | -------------------------------------------------------------------------------- /app/components/home.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | require("../polyfill"); 4 | 5 | import { useState, useEffect, useRef } from "react"; 6 | 7 | import { IconButton } from "./button"; 8 | import styles from "./home.module.scss"; 9 | 10 | import SettingsIcon from "../icons/settings.svg"; 11 | import GithubIcon from "../icons/github.svg"; 12 | import ChatGptIcon from "../icons/chatgpt.svg"; 13 | 14 | import BotIcon from "../icons/bot.svg"; 15 | import AddIcon from "../icons/add.svg"; 16 | import LoadingIcon from "../icons/three-dots.svg"; 17 | import CloseIcon from "../icons/close.svg"; 18 | 19 | import { useChatStore } from "../store"; 20 | import { isMobileScreen } from "../utils"; 21 | import Locale from "../locales"; 22 | import { Chat } from "./chat"; 23 | 24 | import dynamic from "next/dynamic"; 25 | import { REPO_URL } from "../constant"; 26 | import { ErrorBoundary } from "./error"; 27 | 28 | export function Loading(props: { noLogo?: boolean }) { 29 | return ( 30 |
31 | {!props.noLogo && } 32 | 33 |
34 | ); 35 | } 36 | 37 | const Settings = dynamic(async () => (await import("./settings")).Settings, { 38 | loading: () => , 39 | }); 40 | 41 | const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { 42 | loading: () => , 43 | }); 44 | 45 | function useSwitchTheme() { 46 | const config = useChatStore((state) => state.config); 47 | 48 | useEffect(() => { 49 | document.body.classList.remove("light"); 50 | document.body.classList.remove("dark"); 51 | 52 | if (config.theme === "dark") { 53 | document.body.classList.add("dark"); 54 | } else if (config.theme === "light") { 55 | document.body.classList.add("light"); 56 | } 57 | 58 | const metaDescriptionDark = document.querySelector( 59 | 'meta[name="theme-color"][media]', 60 | ); 61 | const metaDescriptionLight = document.querySelector( 62 | 'meta[name="theme-color"]:not([media])', 63 | ); 64 | 65 | if (config.theme === "auto") { 66 | metaDescriptionDark?.setAttribute("content", "#151515"); 67 | metaDescriptionLight?.setAttribute("content", "#fafafa"); 68 | } else { 69 | const themeColor = getComputedStyle(document.body) 70 | .getPropertyValue("--theme-color") 71 | .trim(); 72 | metaDescriptionDark?.setAttribute("content", themeColor); 73 | metaDescriptionLight?.setAttribute("content", themeColor); 74 | } 75 | }, [config.theme]); 76 | } 77 | 78 | function useDragSideBar() { 79 | const limit = (x: number) => Math.min(500, Math.max(220, x)); 80 | 81 | const chatStore = useChatStore(); 82 | const startX = useRef(0); 83 | const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300); 84 | const lastUpdateTime = useRef(Date.now()); 85 | 86 | const handleMouseMove = useRef((e: MouseEvent) => { 87 | if (Date.now() < lastUpdateTime.current + 100) { 88 | return; 89 | } 90 | lastUpdateTime.current = Date.now(); 91 | const d = e.clientX - startX.current; 92 | const nextWidth = limit(startDragWidth.current + d); 93 | chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth)); 94 | }); 95 | 96 | const handleMouseUp = useRef(() => { 97 | startDragWidth.current = chatStore.config.sidebarWidth ?? 300; 98 | window.removeEventListener("mousemove", handleMouseMove.current); 99 | window.removeEventListener("mouseup", handleMouseUp.current); 100 | }); 101 | 102 | const onDragMouseDown = (e: MouseEvent) => { 103 | startX.current = e.clientX; 104 | 105 | window.addEventListener("mousemove", handleMouseMove.current); 106 | window.addEventListener("mouseup", handleMouseUp.current); 107 | }; 108 | 109 | useEffect(() => { 110 | if (isMobileScreen()) { 111 | return; 112 | } 113 | 114 | document.documentElement.style.setProperty( 115 | "--sidebar-width", 116 | `${limit(chatStore.config.sidebarWidth ?? 300)}px`, 117 | ); 118 | }, [chatStore.config.sidebarWidth]); 119 | 120 | return { 121 | onDragMouseDown, 122 | }; 123 | } 124 | 125 | const useHasHydrated = () => { 126 | const [hasHydrated, setHasHydrated] = useState(false); 127 | 128 | useEffect(() => { 129 | setHasHydrated(true); 130 | }, []); 131 | 132 | return hasHydrated; 133 | }; 134 | 135 | function _Home() { 136 | const [createNewSession, currentIndex, removeSession] = useChatStore( 137 | (state) => [ 138 | state.newSession, 139 | state.currentSessionIndex, 140 | state.removeSession, 141 | ], 142 | ); 143 | const chatStore = useChatStore(); 144 | const loading = !useHasHydrated(); 145 | const [showSideBar, setShowSideBar] = useState(true); 146 | 147 | // setting 148 | const [openSettings, setOpenSettings] = useState(false); 149 | const config = useChatStore((state) => state.config); 150 | 151 | // drag side bar 152 | const { onDragMouseDown } = useDragSideBar(); 153 | 154 | useSwitchTheme(); 155 | 156 | if (loading) { 157 | return ; 158 | } 159 | 160 | return ( 161 |
168 |
171 |
172 |
ChatGPT Next
173 |
174 | Build your own AI assistant. 175 |
176 |
177 | 178 |
179 |
180 | 181 |
{ 184 | setOpenSettings(false); 185 | setShowSideBar(false); 186 | }} 187 | > 188 | 189 |
190 | 191 |
192 |
193 |
194 | } 196 | onClick={chatStore.deleteSession} 197 | /> 198 |
199 |
200 | } 202 | onClick={() => { 203 | setOpenSettings(true); 204 | setShowSideBar(false); 205 | }} 206 | shadow 207 | /> 208 |
209 | 214 |
215 |
216 | } 218 | text={Locale.Home.NewChat} 219 | onClick={() => { 220 | createNewSession(); 221 | setShowSideBar(false); 222 | }} 223 | shadow 224 | /> 225 |
226 |
227 | 228 |
onDragMouseDown(e as any)} 231 | >
232 |
233 | 234 |
235 | {openSettings ? ( 236 | { 238 | setOpenSettings(false); 239 | setShowSideBar(true); 240 | }} 241 | /> 242 | ) : ( 243 | setShowSideBar(true)} 246 | sideBarShowing={showSideBar} 247 | /> 248 | )} 249 |
250 |
251 | ); 252 | } 253 | 254 | export function Home() { 255 | return ( 256 | 257 | <_Home> 258 | 259 | ); 260 | } 261 | -------------------------------------------------------------------------------- /app/components/home.module.scss: -------------------------------------------------------------------------------- 1 | @import "./window.scss"; 2 | @import "../styles/animation.scss"; 3 | 4 | @mixin container { 5 | background-color: var(--white); 6 | border: var(--border-in-light); 7 | border-radius: 20px; 8 | box-shadow: var(--shadow); 9 | color: var(--black); 10 | background-color: var(--white); 11 | min-width: 600px; 12 | min-height: 480px; 13 | max-width: 1200px; 14 | 15 | display: flex; 16 | overflow: hidden; 17 | box-sizing: border-box; 18 | 19 | width: var(--window-width); 20 | height: var(--window-height); 21 | } 22 | 23 | .container { 24 | @include container(); 25 | } 26 | 27 | @media only screen and (min-width: 600px) { 28 | .tight-container { 29 | --window-width: 100vw; 30 | --window-height: var(--full-height); 31 | --window-content-width: calc(100% - var(--sidebar-width)); 32 | 33 | @include container(); 34 | 35 | max-width: 100vw; 36 | max-height: var(--full-height); 37 | 38 | border-radius: 0; 39 | } 40 | } 41 | 42 | .sidebar { 43 | top: 0; 44 | width: var(--sidebar-width); 45 | box-sizing: border-box; 46 | padding: 20px; 47 | background-color: var(--second); 48 | display: flex; 49 | flex-direction: column; 50 | box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05); 51 | position: relative; 52 | transition: width ease 0.1s; 53 | } 54 | 55 | .sidebar-drag { 56 | $width: 10px; 57 | 58 | position: absolute; 59 | top: 0; 60 | right: 0; 61 | height: 100%; 62 | width: $width; 63 | background-color: var(--black); 64 | cursor: ew-resize; 65 | opacity: 0; 66 | transition: all ease 0.3s; 67 | 68 | &:hover, 69 | &:active { 70 | opacity: 0.2; 71 | } 72 | } 73 | 74 | .window-content { 75 | width: var(--window-content-width); 76 | height: 100%; 77 | display: flex; 78 | flex-direction: column; 79 | } 80 | 81 | .mobile { 82 | display: none; 83 | } 84 | 85 | @media only screen and (max-width: 600px) { 86 | .container { 87 | min-height: unset; 88 | min-width: unset; 89 | max-height: unset; 90 | min-width: unset; 91 | border: 0; 92 | border-radius: 0; 93 | } 94 | 95 | .sidebar { 96 | position: absolute; 97 | left: -100%; 98 | z-index: 1000; 99 | height: var(--full-height); 100 | transition: all ease 0.3s; 101 | box-shadow: none; 102 | } 103 | 104 | .sidebar-show { 105 | left: 0; 106 | } 107 | 108 | .mobile { 109 | display: block; 110 | } 111 | } 112 | 113 | .sidebar-header { 114 | position: relative; 115 | padding-top: 20px; 116 | padding-bottom: 20px; 117 | } 118 | 119 | .sidebar-logo { 120 | position: absolute; 121 | right: 0; 122 | bottom: 18px; 123 | } 124 | 125 | .sidebar-title { 126 | font-size: 20px; 127 | font-weight: bold; 128 | } 129 | 130 | .sidebar-sub-title { 131 | font-size: 12px; 132 | font-weight: 400px; 133 | } 134 | 135 | .sidebar-body { 136 | flex: 1; 137 | overflow: auto; 138 | } 139 | 140 | .chat-list { 141 | } 142 | 143 | .chat-item { 144 | padding: 10px 14px; 145 | background-color: var(--white); 146 | border-radius: 10px; 147 | margin-bottom: 10px; 148 | box-shadow: var(--card-shadow); 149 | transition: background-color 0.3s ease; 150 | cursor: pointer; 151 | user-select: none; 152 | border: 2px solid transparent; 153 | position: relative; 154 | overflow: hidden; 155 | } 156 | 157 | .chat-item:hover { 158 | background-color: var(--hover-color); 159 | } 160 | 161 | .chat-item-selected { 162 | border-color: var(--primary); 163 | } 164 | 165 | .chat-item-title { 166 | font-size: 14px; 167 | font-weight: bolder; 168 | display: block; 169 | width: 200px; 170 | overflow: hidden; 171 | text-overflow: ellipsis; 172 | white-space: nowrap; 173 | } 174 | 175 | .chat-item-delete { 176 | position: absolute; 177 | top: 10px; 178 | right: -20px; 179 | transition: all ease 0.3s; 180 | opacity: 0; 181 | cursor: pointer; 182 | } 183 | 184 | .chat-item:hover > .chat-item-delete { 185 | opacity: 0.5; 186 | right: 10px; 187 | } 188 | 189 | .chat-item:hover > .chat-item-delete:hover { 190 | opacity: 1; 191 | } 192 | 193 | .chat-item-info { 194 | display: flex; 195 | justify-content: space-between; 196 | color: rgb(166, 166, 166); 197 | font-size: 12px; 198 | margin-top: 8px; 199 | } 200 | 201 | .chat-item-count, 202 | .chat-item-date { 203 | overflow: hidden; 204 | text-overflow: ellipsis; 205 | white-space: nowrap; 206 | } 207 | 208 | .sidebar-tail { 209 | display: flex; 210 | justify-content: space-between; 211 | padding-top: 20px; 212 | } 213 | 214 | .sidebar-actions { 215 | display: inline-flex; 216 | } 217 | 218 | .sidebar-action:not(:last-child) { 219 | margin-right: 15px; 220 | } 221 | 222 | .chat { 223 | display: flex; 224 | flex-direction: column; 225 | position: relative; 226 | height: 100%; 227 | } 228 | 229 | .chat-body { 230 | flex: 1; 231 | overflow: auto; 232 | padding: 20px; 233 | position: relative; 234 | } 235 | 236 | .chat-body-title { 237 | cursor: pointer; 238 | 239 | &:hover { 240 | text-decoration: underline; 241 | } 242 | } 243 | 244 | .chat-message { 245 | display: flex; 246 | flex-direction: row; 247 | } 248 | 249 | .chat-message-user { 250 | display: flex; 251 | flex-direction: row-reverse; 252 | } 253 | 254 | .chat-message-container { 255 | max-width: var(--message-max-width); 256 | display: flex; 257 | flex-direction: column; 258 | align-items: flex-start; 259 | animation: slide-in ease 0.3s; 260 | 261 | &:hover { 262 | .chat-message-top-actions { 263 | opacity: 1; 264 | right: 10px; 265 | pointer-events: all; 266 | } 267 | } 268 | } 269 | 270 | .chat-message-user > .chat-message-container { 271 | align-items: flex-end; 272 | } 273 | 274 | .chat-message-avatar { 275 | margin-top: 20px; 276 | } 277 | 278 | .chat-message-status { 279 | font-size: 12px; 280 | color: #aaa; 281 | line-height: 1.5; 282 | margin-top: 5px; 283 | } 284 | 285 | .user-avtar { 286 | height: 30px; 287 | width: 30px; 288 | display: flex; 289 | align-items: center; 290 | justify-content: center; 291 | border: var(--border-in-light); 292 | box-shadow: var(--card-shadow); 293 | border-radius: 10px; 294 | } 295 | 296 | .chat-message-item { 297 | box-sizing: border-box; 298 | max-width: 100%; 299 | margin-top: 10px; 300 | border-radius: 10px; 301 | background-color: rgba(0, 0, 0, 0.05); 302 | padding: 10px; 303 | font-size: 14px; 304 | user-select: text; 305 | word-break: break-word; 306 | border: var(--border-in-light); 307 | position: relative; 308 | } 309 | 310 | .chat-message-top-actions { 311 | font-size: 12px; 312 | position: absolute; 313 | right: 20px; 314 | top: -26px; 315 | left: 100px; 316 | transition: all ease 0.3s; 317 | opacity: 0; 318 | pointer-events: none; 319 | 320 | display: flex; 321 | flex-direction: row-reverse; 322 | 323 | .chat-message-top-action { 324 | opacity: 0.5; 325 | color: var(--black); 326 | white-space: nowrap; 327 | cursor: pointer; 328 | 329 | &:hover { 330 | opacity: 1; 331 | } 332 | 333 | &:not(:first-child) { 334 | margin-right: 10px; 335 | } 336 | } 337 | } 338 | 339 | .chat-message-user > .chat-message-container > .chat-message-item { 340 | background-color: var(--second); 341 | } 342 | 343 | .chat-message-actions { 344 | display: flex; 345 | flex-direction: row-reverse; 346 | width: 100%; 347 | padding-top: 5px; 348 | box-sizing: border-box; 349 | font-size: 12px; 350 | } 351 | 352 | .chat-message-action-date { 353 | color: #aaa; 354 | } 355 | 356 | .chat-input-panel { 357 | width: 100%; 358 | padding: 20px; 359 | padding-top: 5px; 360 | box-sizing: border-box; 361 | flex-direction: column; 362 | } 363 | 364 | @mixin single-line { 365 | white-space: nowrap; 366 | overflow: hidden; 367 | text-overflow: ellipsis; 368 | } 369 | 370 | .prompt-hints { 371 | min-height: 20px; 372 | width: 100%; 373 | max-height: 50vh; 374 | overflow: auto; 375 | display: flex; 376 | flex-direction: column-reverse; 377 | 378 | background-color: var(--white); 379 | border: var(--border-in-light); 380 | border-radius: 10px; 381 | margin-bottom: 10px; 382 | box-shadow: var(--shadow); 383 | 384 | .prompt-hint { 385 | color: var(--black); 386 | padding: 6px 10px; 387 | animation: slide-in ease 0.3s; 388 | cursor: pointer; 389 | transition: all ease 0.3s; 390 | border: transparent 1px solid; 391 | margin: 4px; 392 | border-radius: 8px; 393 | 394 | &:not(:last-child) { 395 | margin-top: 0; 396 | } 397 | 398 | .hint-title { 399 | font-size: 12px; 400 | font-weight: bolder; 401 | 402 | @include single-line(); 403 | } 404 | .hint-content { 405 | font-size: 12px; 406 | 407 | @include single-line(); 408 | } 409 | 410 | &-selected, 411 | &:hover { 412 | border-color: var(--primary); 413 | } 414 | } 415 | } 416 | 417 | .chat-input-panel-inner { 418 | display: flex; 419 | flex: 1; 420 | } 421 | 422 | .chat-input { 423 | height: 100%; 424 | width: 100%; 425 | border-radius: 10px; 426 | border: var(--border-in-light); 427 | box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03); 428 | background-color: var(--white); 429 | color: var(--black); 430 | font-family: inherit; 431 | padding: 10px 90px 10px 14px; 432 | resize: none; 433 | outline: none; 434 | } 435 | 436 | .chat-input:focus { 437 | border: 1px solid var(--primary); 438 | } 439 | 440 | .chat-input-send { 441 | background-color: var(--primary); 442 | color: white; 443 | 444 | position: absolute; 445 | right: 30px; 446 | bottom: 32px; 447 | } 448 | 449 | @media only screen and (max-width: 600px) { 450 | .chat-input { 451 | font-size: 16px; 452 | } 453 | 454 | .chat-input-send { 455 | bottom: 30px; 456 | } 457 | } 458 | 459 | .export-content { 460 | white-space: break-spaces; 461 | padding: 10px !important; 462 | } 463 | 464 | .loading-content { 465 | display: flex; 466 | flex-direction: column; 467 | justify-content: center; 468 | align-items: center; 469 | height: 100%; 470 | width: 100%; 471 | } 472 | -------------------------------------------------------------------------------- /docs/faq-en.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## How to get help quickly? 4 | 1. Ask ChatGPT / Bing / Baidu / Google, etc. 5 | 2. Ask online friends. Please provide background information and a detailed description of the problem. High-quality questions are more likely to get useful answers. 6 | 7 | # Deployment Related Questions 8 | 9 | ## Why does the Docker deployment version always prompt for updates 10 | The Docker version is equivalent to the stable version, and the latest Docker is always consistent with the latest release version. Currently, our release frequency is once every one to two days, so the Docker version will always be one to two days behind the latest commit, which is expected. 11 | 12 | ## How to deploy on Vercel 13 | 1. Register a Github account and fork this project. 14 | 2. Register Vercel (mobile phone verification required, Chinese number can be used), and connect your Github account. 15 | 3. Create a new project on Vercel, select the project you forked on Github, fill in the required environment variables, and start deploying. After deployment, you can access your project through the domain provided by Vercel. (Requires proxy in mainland China) 16 | * If you need to access it directly in China: At your DNS provider, add a CNAME record for the domain name, pointing to cname.vercel-dns.com. Then set up your domain access on Vercel. 17 | 18 | ## How to modify Vercel environment variables 19 | - Enter the Vercel console page; 20 | - Select your chatgpt-next-web project; 21 | - Click on the Settings option at the top of the page; 22 | - Find the Environment Variables option in the sidebar; 23 | - Modify the corresponding values as needed. 24 | 25 | ## What is the environment variable CODE? Is it necessary to set it? 26 | This is your custom access password, you can choose: 27 | 1. Do not set it, delete the environment variable. Be cautious: anyone can access your project at this time. 28 | 2. When deploying the project, set the environment variable CODE (supports multiple passwords, separated by commas). After setting the access password, users need to enter the access password in the settings page to use it. See [related instructions](https://github.com/Yidadaa/ChatGPT-Next-Web#access-password) 29 | 30 | ## Why doesn't the version I deployed have streaming response 31 | > Related discussion: [#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386) 32 | 33 | If you use nginx reverse proxy, you need to add the following code to the configuration file: 34 | ``` 35 | # No caching, support streaming output 36 | proxy_cache off; # Turn off caching 37 | proxy_buffering off; # Turn off proxy buffering 38 | chunked_transfer_encoding on; # Turn on chunked transfer encoding 39 | tcp_nopush on; # Turn on TCP NOPUSH option, disable Nagle algorithm 40 | tcp_nodelay on; # Turn on TCP NODELAY option, disable delay ACK algorithm 41 | keepalive_timeout 300; # Set keep-alive timeout to 65 seconds 42 | ``` 43 | 44 | If you are deploying on netlify, this issue is still waiting to be resolved, please be patient. 45 | 46 | ## I've deployed, but it's not accessible 47 | Please check and troubleshoot the following issues: 48 | - Is the service started? 49 | - Is the port correctly mapped? 50 | - Is the firewall port open? 51 | - Is the route to the server okay? 52 | - Is the domain name resolved correctly? 53 | 54 | # Usage Related Questions 55 | 56 | ## Why does it always prompt "An error occurred, please try again later" 57 | There could be many reasons, please check the following in order: 58 | - First, check if your code version is the latest version, update to the latest version and try again; 59 | - Check if the api key is set correctly, the environment variable name must be uppercase with underscores; 60 | - Check if the api key is available; 61 | - If you still cannot determine the problem after going through the above steps, please submit a new issue in the issue area and attach the runtime log of vercel or the log of docker runtime. 62 | 63 | ## Why does ChatGPT's reply get garbled 64 | In the settings page - model settings, there is an item called `temperature`. If this value is greater than 1, it may cause garbled replies. Adjust it back to within 1. 65 | 66 | ## It prompts "Now it's unauthorized, please enter the access password on the settings page" when using? 67 | The project has set an access password through the environment variable CODE. When using it for the first time, you need to go to settings and enter the access code to use. 68 | 69 | ## It prompts "You exceeded your current quota, ..." when using? 70 | The API KEY is problematic. Insufficient balance. 71 | 72 | ## What is a proxy and how to use it? 73 | Due to IP restrictions of OpenAI, China and some other countries/regions cannot directly connect to OpenAI API and need to go through a proxy. You can use a proxy server (forward proxy) or a pre-configured OpenAI API reverse proxy. 74 | - Forward proxy example: VPN ladder. In the case of docker deployment, set the environment variable HTTP_PROXY to your proxy address (http://address:port). 75 | - Reverse proxy example: You can use someone else's proxy address or set it up for free through Cloudflare. Set the project environment variable BASE_URL to your proxy address. 76 | 77 | ## Can I deploy it on a server in China? 78 | It is possible but there are issues to be addressed: 79 | - Proxy is required to connect to websites such as Github and OpenAI; 80 | - Domain name resolution requires filing for servers in China; 81 | - Chinese policy restricts proxy access to foreign websites/ChatGPT-related applications, which may be blocked. 82 | 83 | # Network Service Related Questions 84 | ## What is Cloudflare? 85 | Cloudflare (CF) is a network service provider offering CDN, domain management, static page hosting, edge computing function deployment, and more. Common use cases: purchase and/or host your domain (resolution, dynamic domain, etc.), apply CDN to your server (can hide IP to avoid being blocked), deploy websites (CF Pages). CF offers most services for free. 86 | 87 | ## What is Vercel? 88 | Vercel is a global cloud platform designed to help developers build and deploy modern web applications more quickly. This project and many web applications can be deployed on Vercel with a single click for free. No need to understand code, Linux, have a server, pay, or set up an OpenAI API proxy. The downside is that you need to bind a domain name to access it without restrictions in China. 89 | 90 | ## How to obtain a domain name? 91 | 1. Register with a domain provider, such as Namesilo (supports Alipay) or Cloudflare for international providers, and Wanwang for domestic providers in China. 92 | 2. Free domain name providers: eu.org (second-level domain), etc. 93 | 3. Ask friends for a free second-level domain. 94 | 95 | ## How to obtain a server 96 | - Examples of international server providers: Amazon Web Services, Google Cloud, Vultr, Bandwagon, Hostdare, etc. 97 | International server considerations: Server lines affect access speed in China; CN2 GIA and CN2 lines are recommended. If the server has difficulty accessing in China (serious packet loss, etc.), you can try using a CDN (from providers like Cloudflare). 98 | - Domestic server providers: Alibaba Cloud, Tencent, etc. 99 | Domestic server considerations: Domain name resolution requires filing; domestic server bandwidth is relatively expensive; accessing foreign websites (Github, OpenAI, etc.) requires a proxy. 100 | 101 | # OpenAI-related Questions 102 | ## How to register an OpenAI account? 103 | Go to chat.openai.com to register. You will need: 104 | - A good VPN (OpenAI only allows native IP addresses of supported regions) 105 | - A supported email (e.g., Gmail or a company/school email, not Outlook or QQ email) 106 | - A way to receive SMS verification (e.g., SMS-activate website) 107 | 108 | ## How to activate OpenAI API? How to check API balance? 109 | Official website (requires VPN): https://platform.openai.com/account/usage 110 | Some users have set up a proxy to check the balance without a VPN; ask online friends for access. Please verify the source is reliable to avoid API Key leakage. 111 | 112 | ## Why doesn't my new OpenAI account have an API balance? 113 | (Updated April 6th) Newly registered accounts usually display API balance within 24 hours. New accounts are currently given a $5 balance. 114 | 115 | ## How to recharge OpenAI API? 116 | OpenAI only accepts credit cards from designated regions (Chinese credit cards cannot be used). If the credit cards from your region is not supported, some options include: 117 | 1. Depay virtual credit card 118 | 2. Apply for a foreign credit card 119 | 3. Find someone online to top up 120 | 121 | ## How to access the GPT-4 API? 122 | (Updated April 6th) Access to the GPT-4 API requires a separate application. Go to the following address and enter your information to join the waitlist (prepare your OpenAI organization ID): https://openai.com/waitlist/gpt-4-api 123 | Wait for email updates afterwards. 124 | 125 | ## How to use the Azure OpenAI interface 126 | Please refer to: [#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371) 127 | 128 | ## Why is my Token consumed so fast? 129 | > Related discussion: [#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518) 130 | - If you have GPT-4 access and use GPT-4 API regularly, your bill will increase rapidly since GPT-4 pricing is about 15 times higher than GPT-3.5; 131 | - If you are using GPT-3.5 and not using it frequently, but still find your bill increasing fast, please troubleshoot immediately using these steps: 132 | - Check your API key consumption record on the OpenAI website; if your token is consumed every hour and each time consumes tens of thousands of tokens, your key must have been leaked. Please delete it and regenerate it immediately. **Do not check your balance on random websites.** 133 | - If your password is short, such as 5 characters or fewer, the cost of brute-forcing is very low. It is recommended to search docker logs to confirm whether someone has tried a large number of password combinations. Keyword: got access code 134 | - By following these two methods, you can locate the reason for your token's rapid consumption: 135 | - If the OpenAI consumption record is abnormal but the Docker log has no issues, it means your API key has been leaked; 136 | - If the Docker log shows a large number of got access code brute-force attempts, your password has been cracked. 137 | --------------------------------------------------------------------------------