diff --git a/.env.template b/.env.template index 5f3fc02da..0f4bf0e7c 100644 --- a/.env.template +++ b/.env.template @@ -27,3 +27,8 @@ HIDE_USER_API_KEY= # Default: Empty # If you do not want users to use GPT-4, set this value to 1. DISABLE_GPT4= + +# (optional) +# Default: Empty +# If you do not want users to query balance, set this value to 1. +HIDE_BALANCE_QUERY= \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..3a3cce576 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml new file mode 100644 index 000000000..b928ad6c1 --- /dev/null +++ b/.github/workflows/app.yml @@ -0,0 +1,105 @@ +name: Release App + +on: + workflow_dispatch: + release: + types: [published] + +jobs: + create-release: + permissions: + contents: write + runs-on: ubuntu-latest + outputs: + release_id: ${{ steps.create-release.outputs.result }} + + steps: + - uses: actions/checkout@v3 + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: get version + run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV + - name: create release + id: create-release + uses: actions/github-script@v6 + with: + script: | + const { data } = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + }) + return data.id + + build-tauri: + needs: create-release + permissions: + contents: write + strategy: + fail-fast: false + matrix: + config: + - os: ubuntu-latest + arch: x86_64 + rust_target: x86_64-unknown-linux-gnu + - os: macos-latest + arch: x86_64 + rust_target: x86_64-apple-darwin + - os: macos-latest + arch: aarch64 + rust_target: aarch64-apple-darwin + - os: windows-latest + arch: x86_64 + rust_target: x86_64-pc-windows-msvc + + runs-on: ${{ matrix.config.os }} + steps: + - uses: actions/checkout@v3 + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.config.rust_target }} + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.config.rust_target }} + - name: install dependencies (ubuntu only) + if: matrix.config.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + - name: install frontend dependencies + run: yarn install # change this to npm or pnpm depending on which one you use + - uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + with: + releaseId: ${{ needs.create-release.outputs.release_id }} + + publish-release: + permissions: + contents: write + runs-on: ubuntu-latest + needs: [create-release, build-tauri] + + steps: + - name: publish release + id: publish-release + uses: actions/github-script@v6 + env: + release_id: ${{ needs.create-release.outputs.release_id }} + with: + script: | + github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: process.env.release_id, + draft: false, + prerelease: false + }) diff --git a/.github/workflows/issue-translator.yml b/.github/workflows/issue-translator.yml new file mode 100644 index 000000000..560f66d34 --- /dev/null +++ b/.github/workflows/issue-translator.yml @@ -0,0 +1,15 @@ +name: Issue Translator +on: + issue_comment: + types: [created] + issues: + types: [opened] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: usthe/issues-translate-action@v2.7 + with: + IS_MODIFY_TITLE: false + CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index a4c14c843..ebf5587d0 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -35,6 +35,6 @@ jobs: - name: Sync check if: failure() run: | - echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。" - echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]." + echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" + echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates" exit 1 diff --git a/.gitignore b/.gitignore index 446a21433..b00b0e325 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ dev # docker-compose env files .env + +*.key +*.key.pub \ No newline at end of file diff --git a/README.md b/README.md index 9607a21ef..1ca376562 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,25 @@ English / [简体中文](./README_CN.md) -One-Click to deploy well-designed ChatGPT web UI on Vercel. +One-Click to get well-designed cross-platform ChatGPT web UI. -一键免费部署你的私人 ChatGPT 网页应用。 +一键免费部署你的跨平台私人 ChatGPT 应用。 -[Demo](https://chatgpt.nextweb.fun/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) +[![Web][Web-image]][web-url] +[![Windows][Windows-image]][download-url] +[![MacOS][MacOS-image]][download-url] +[![Linux][Linux-image]][download-url] -[演示](https://chatgpt.nextweb.fun/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) +[Web App](https://chatgpt.nextweb.fun/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) + +[网页版](https://chatgpt.nextweb.fun/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) + +[web-url]: https://chatgpt.nextweb.fun +[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases +[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge +[Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows +[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple +[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu [![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) @@ -24,6 +36,8 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. ## Features - **Deploy for free with one-click** on Vercel in under 1 minute +- Compact client (~5MB) on Linux/Windows/MacOS, [download it now](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) +- Fully compatible with self-deployed llms, recommended for use with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) or [LocalAI](https://github.com/go-skynet/LocalAI) - Privacy first, all data stored locally in the browser - Markdown support: LaTex, mermaid, code highlight, etc. - Responsive design, dark mode and PWA @@ -39,23 +53,20 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - [x] User Prompt: user can edit and save custom prompts to prompt list - [x] Prompt Template: create a new chat with pre-defined in-context prompts [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993) - [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741) -- [ ] Desktop App with tauri -- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. +- [x] Desktop App with tauri +- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc. - [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) -### Not in Plan - -- User login, accounts, cloud sync -- UI text customize - ## What's New - 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/). - 🚀 v2.7 let's share conversations as image, or share to ShareGPT! +- 🚀 v2.8 now we have a client that runs across all platforms! ## 主要功能 - 在 1 分钟内使用 Vercel **免费一键部署** +- 提供体积极小(~5MB)的跨平台客户端(Linux/Windows/MacOS), [下载地址](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) - 完整的 Markdown 支持:LaTex 公式、Mermaid 流程图、代码高亮等等 - 精心设计的 UI,响应式设计,支持深色模式,支持 PWA - 极快的首屏加载速度(~100kb),支持流式响应 @@ -72,20 +83,16 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - [x] 允许用户自行编辑内置 Prompt 列表 - [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993) - [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741) -- [ ] 使用 tauri 打包桌面应用 -- [ ] 支持自部署的大语言模型 +- [x] 使用 tauri 打包桌面应用 +- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm) - [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) -### 不会开发的功能 - -- 界面文字自定义 -- 用户登录、账号管理、消息云同步 - ## 最新动态 - 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。 - 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com - 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。 +- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。 ## Get Started @@ -178,6 +185,16 @@ If you do not want users to input their own API key, set this value to 1. If you do not want users to use GPT-4, set this value to 1. +### `HIDE_BALANCE_QUERY` (optional) + +> Default: Empty + +If you do not want users to query balance, set this value to 1. + +## Requirements + +NodeJS >= 18, Docker >= 20 + ## Development > [简体中文 > 如何进行二次开发](./README_CN.md#开发) @@ -188,6 +205,9 @@ Before starting development, you must create a new `.env.local` file at project ``` OPENAI_API_KEY= + +# if you are not able to access openai service, use this BASE_URL +BASE_URL=https://chatgpt1.nextweb.fun/api/proxy ``` ### Local Development @@ -225,6 +245,12 @@ docker run -d -p 3000:3000 \ yidadaa/chatgpt-next-web ``` +If your proxy needs password, use: + +```shell +-e PROXY_URL="http://127.0.0.1:7890 user pass" +``` + ### Shell ```shell @@ -237,6 +263,10 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ![More](./docs/images/more.png) +## Translation + +If you want to add a new translation, read this [document](./docs/translation.md). + ## Donation [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) @@ -268,6 +298,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s [@Sha1rholder](https://github.com/Sha1rholder) [@AnsonHyq](https://github.com/AnsonHyq) [@synwith](https://github.com/synwith) +[@piksonGit](https://github.com/piksonGit) ### Contributor diff --git a/README_CN.md b/README_CN.md index 9601e5fda..990b64424 100644 --- a/README_CN.md +++ b/README_CN.md @@ -98,9 +98,11 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。 -## 开发 +### `HIDE_BALANCE_QUERY` (可选) -> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。 +如果你不想让用户查询余额,将此环境变量设置为 1 即可。 + +## 开发 点击下方按钮,开始二次开发: @@ -110,13 +112,16 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 ``` OPENAI_API_KEY= + +# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址 +BASE_URL=https://chatgpt1.nextweb.fun/api/proxy ``` ### 本地开发 1. 安装 nodejs 18 和 yarn,具体细节请询问 ChatGPT; 2. 执行 `yarn install && yarn dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署! -3. 如果你想本地部署,请使用 `yarn install && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。 +3. 如果你想本地部署,请使用 `yarn install && yarn build && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。 ## 部署 @@ -146,6 +151,12 @@ docker run -d -p 3000:3000 \ yidadaa/chatgpt-next-web ``` +如果你的本地代理需要账号密码,可以使用: + +```shell +-e PROXY_URL="http://127.0.0.1:7890 user password" +``` + 如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。 ### 本地部署 diff --git a/README_ES.md b/README_ES.md index cdd835908..e9705e402 100644 --- a/README_ES.md +++ b/README_ES.md @@ -96,6 +96,10 @@ Si no desea que los usuarios rellenen la clave de API ellos mismos, establezca e Si no desea que los usuarios utilicen GPT-4, establezca esta variable de entorno en 1. +### `HIDE_BALANCE_QUERY` (Opcional) + +Si no desea que los usuarios consulte el saldo, establezca esta variable de entorno en 1. + ## explotación > No se recomienda encarecidamente desarrollar o implementar localmente, debido a algunas razones técnicas, es difícil configurar el agente API de OpenAI localmente, a menos que pueda asegurarse de que puede conectarse directamente al servidor OpenAI. diff --git a/app/api/auth.ts b/app/api/auth.ts index fffb63c1f..e0453b2b4 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -2,7 +2,6 @@ import { NextRequest } from "next/server"; import { getServerSideConfig } from "../config/server"; import md5 from "spark-md5"; import { ACCESS_CODE_PREFIX } from "../constant"; -import { OPENAI_URL } from "./common"; function getIP(req: NextRequest) { let ip = req.ip ?? req.headers.get("x-real-ip"); diff --git a/app/api/common.ts b/app/api/common.ts index ccfb99e26..6d6a7d1fb 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -35,14 +35,16 @@ export async function requestOpenai(req: NextRequest) { const fetchOptions: RequestInit = { headers: { "Content-Type": "application/json", + "Cache-Control": "no-store", Authorization: authValue, ...(process.env.OPENAI_ORG_ID && { "OpenAI-Organization": process.env.OPENAI_ORG_ID, }), }, - cache: "no-store", method: req.method, body: req.body, + // @ts-ignore + duplex: "half", signal: controller.signal, }; @@ -76,8 +78,7 @@ export async function requestOpenai(req: NextRequest) { // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); newHeaders.delete("www-authenticate"); - - // to disbale ngnix buffering + // to disable nginx buffering newHeaders.set("X-Accel-Buffering", "no"); return new Response(res.body, { diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 2b3bcbf20..7749e6e9e 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -9,7 +9,8 @@ const serverConfig = getServerSideConfig(); const DANGER_CONFIG = { needCode: serverConfig.needCode, hideUserApiKey: serverConfig.hideUserApiKey, - enableGPT4: serverConfig.enableGPT4, + disableGPT4: serverConfig.disableGPT4, + hideBalanceQuery: serverConfig.hideBalanceQuery, }; declare global { diff --git a/app/api/openai/[...path]/route.ts b/app/api/openai/[...path]/route.ts index 04f3b6da8..9df005a31 100644 --- a/app/api/openai/[...path]/route.ts +++ b/app/api/openai/[...path]/route.ts @@ -1,3 +1,5 @@ +import { type OpenAIListModelResponse } from "@/app/client/platforms/openai"; +import { getServerSideConfig } from "@/app/config/server"; import { OpenaiPath } from "@/app/constant"; import { prettyObject } from "@/app/utils/format"; import { NextRequest, NextResponse } from "next/server"; @@ -6,12 +8,28 @@ import { requestOpenai } from "../../common"; const ALLOWD_PATH = new Set(Object.values(OpenaiPath)); +function getModels(remoteModelRes: OpenAIListModelResponse) { + const config = getServerSideConfig(); + + if (config.disableGPT4) { + remoteModelRes.data = remoteModelRes.data.filter( + (m) => !m.id.startsWith("gpt-4"), + ); + } + + return remoteModelRes; +} + async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { console.log("[OpenAI Route] params ", params); + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + const subpath = params.path.join("/"); if (!ALLOWD_PATH.has(subpath)) { @@ -35,7 +53,18 @@ async function handle( } try { - return await requestOpenai(req); + const response = await requestOpenai(req); + + // list models + if (subpath === OpenaiPath.ListModelPath && response.status === 200) { + const resJson = (await response.json()) as OpenAIListModelResponse; + const availableModels = getModels(resJson); + return NextResponse.json(availableModels, { + status: response.status, + }); + } + + return response; } catch (e) { console.error("[OpenAI] ", e); return NextResponse.json(prettyObject(e)); diff --git a/app/client/api.ts b/app/client/api.ts index 8897d7a66..b04dd88b8 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -1,3 +1,4 @@ +import { getClientConfig } from "../config/client"; import { ACCESS_CODE_PREFIX } from "../constant"; import { ChatMessage, ModelType, useAccessStore } from "../store"; import { ChatGPTApi } from "./platforms/openai"; @@ -37,9 +38,15 @@ export interface LLMUsage { total: number; } +export interface LLMModel { + name: string; + available: boolean; +} + export abstract class LLMApi { abstract chat(options: ChatOptions): Promise; abstract usage(): Promise; + abstract models(): Promise; } type ProviderName = "openai" | "azure" | "claude" | "palm"; @@ -92,8 +99,12 @@ export class ClientApi { // 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用 // Please do not modify this message - console.log("[Share]", msgs); - const res = await fetch("/sharegpt", { + console.log("[Share]", messages, msgs); + const clientConfig = getClientConfig(); + const proxyUrl = "/sharegpt"; + const rawUrl = "https://sharegpt.com/api/conversations"; + const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl; + const res = await fetch(shareUrl, { body: JSON.stringify({ avatarUrl, items: msgs, diff --git a/app/client/controller.ts b/app/client/controller.ts index 86cb99e7f..a2e00173d 100644 --- a/app/client/controller.ts +++ b/app/client/controller.ts @@ -3,17 +3,17 @@ export const ChatControllerPool = { controllers: {} as Record, addController( - sessionIndex: number, - messageId: number, + sessionId: string, + messageId: string, controller: AbortController, ) { - const key = this.key(sessionIndex, messageId); + const key = this.key(sessionId, messageId); this.controllers[key] = controller; return key; }, - stop(sessionIndex: number, messageId: number) { - const key = this.key(sessionIndex, messageId); + stop(sessionId: string, messageId: string) { + const key = this.key(sessionId, messageId); const controller = this.controllers[key]; controller?.abort(); }, @@ -26,12 +26,12 @@ export const ChatControllerPool = { return Object.values(this.controllers).length > 0; }, - remove(sessionIndex: number, messageId: number) { - const key = this.key(sessionIndex, messageId); + remove(sessionId: string, messageId: string) { + const key = this.key(sessionId, messageId); delete this.controllers[key]; }, - key(sessionIndex: number, messageIndex: number) { - return `${sessionIndex},${messageIndex}`; + key(sessionId: string, messageIndex: string) { + return `${sessionId},${messageIndex}`; }, }; diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index fd4c33655..e140a1ef5 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -1,7 +1,12 @@ -import { OpenaiPath, REQUEST_TIMEOUT_MS } from "@/app/constant"; +import { + DEFAULT_API_HOST, + DEFAULT_MODELS, + OpenaiPath, + REQUEST_TIMEOUT_MS, +} from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; -import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api"; +import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api"; import Locale from "../../locales"; import { EventStreamContentType, @@ -9,12 +14,29 @@ import { } from "@fortaine/fetch-event-source"; import { prettyObject } from "@/app/utils/format"; +export interface OpenAIListModelResponse { + object: string; + data: Array<{ + id: string; + object: string; + root: string; + }>; +} + export class ChatGPTApi implements LLMApi { + private disableListModels = true; + path(path: string): string { let openaiUrl = useAccessStore.getState().openaiUrl; + if (openaiUrl.length === 0) { + openaiUrl = DEFAULT_API_HOST; + } if (openaiUrl.endsWith("/")) { openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1); } + if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith("/api/openai")) { + openaiUrl = "https://" + openaiUrl; + } return [openaiUrl, path].join("/"); } @@ -42,6 +64,8 @@ export class ChatGPTApi implements LLMApi { model: modelConfig.model, temperature: modelConfig.temperature, presence_penalty: modelConfig.presence_penalty, + frequency_penalty: modelConfig.frequency_penalty, + top_p: modelConfig.top_p, }; console.log("[Request] openai payload: ", requestPayload); @@ -223,5 +247,31 @@ export class ChatGPTApi implements LLMApi { total: total.hard_limit_usd, } as LLMUsage; } + + async models(): Promise { + if (this.disableListModels) { + return DEFAULT_MODELS.slice(); + } + + const res = await fetch(this.path(OpenaiPath.ListModelPath), { + method: "GET", + headers: { + ...getHeaders(), + }, + }); + + const resJson = (await res.json()) as OpenAIListModelResponse; + const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-")); + console.log("[Models]", chatModels); + + if (!chatModels) { + return []; + } + + return chatModels.map((m) => ({ + name: m.id, + available: true, + })); + } } export { OpenaiPath }; diff --git a/app/command.ts b/app/command.ts index 40bad92b3..9330d4ff5 100644 --- a/app/command.ts +++ b/app/command.ts @@ -1,4 +1,6 @@ +import { useEffect } from "react"; import { useSearchParams } from "react-router-dom"; +import Locale from "./locales"; type Command = (param: string) => void; interface Commands { @@ -10,19 +12,62 @@ interface Commands { export function useCommand(commands: Commands = {}) { const [searchParams, setSearchParams] = useSearchParams(); - if (commands === undefined) return; + useEffect(() => { + let shouldUpdate = false; + searchParams.forEach((param, name) => { + const commandName = name as keyof Commands; + if (typeof commands[commandName] === "function") { + commands[commandName]!(param); + searchParams.delete(name); + shouldUpdate = true; + } + }); - let shouldUpdate = false; - searchParams.forEach((param, name) => { - const commandName = name as keyof Commands; - if (typeof commands[commandName] === "function") { - commands[commandName]!(param); - searchParams.delete(name); - shouldUpdate = true; + if (shouldUpdate) { + setSearchParams(searchParams); } - }); - - if (shouldUpdate) { - setSearchParams(searchParams); - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchParams, commands]); +} + +interface ChatCommands { + new?: Command; + newm?: Command; + next?: Command; + prev?: Command; + clear?: Command; + del?: Command; +} + +export const ChatCommandPrefix = ":"; + +export function useChatCommand(commands: ChatCommands = {}) { + function extract(userInput: string) { + return ( + userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput + ) as keyof ChatCommands; + } + + function search(userInput: string) { + const input = extract(userInput); + const desc = Locale.Chat.Commands; + return Object.keys(commands) + .filter((c) => c.startsWith(input)) + .map((c) => ({ + title: desc[c as keyof ChatCommands], + content: ChatCommandPrefix + c, + })); + } + + function match(userInput: string) { + const command = extract(userInput); + const matched = typeof commands[command] === "function"; + + return { + matched, + invoke: () => matched && commands[command]!(userInput), + }; + } + + return { match, search }; } diff --git a/app/components/button.module.scss b/app/components/button.module.scss index 5aa53dcf9..e332df2d2 100644 --- a/app/components/button.module.scss +++ b/app/components/button.module.scss @@ -27,6 +27,26 @@ fill: white !important; } } + + &.danger { + color: rgba($color: red, $alpha: 0.8); + border-color: rgba($color: red, $alpha: 0.5); + background-color: rgba($color: red, $alpha: 0.05); + + &:hover { + border-color: red; + background-color: rgba($color: red, $alpha: 0.1); + } + + path { + fill: red !important; + } + } + + &:hover, + &:focus { + border-color: var(--primary); + } } .shadow { @@ -37,10 +57,6 @@ border: var(--border-in-light); } -.icon-button:hover { - border-color: var(--primary); -} - .icon-button-icon { width: 16px; height: 16px; @@ -56,9 +72,12 @@ } .icon-button-text { - margin-left: 5px; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + + &:not(:first-child) { + margin-left: 5px; + } } diff --git a/app/components/button.tsx b/app/components/button.tsx index f93741b39..7a5633924 100644 --- a/app/components/button.tsx +++ b/app/components/button.tsx @@ -2,16 +2,20 @@ import * as React from "react"; import styles from "./button.module.scss"; +export type ButtonType = "primary" | "danger" | null; + export function IconButton(props: { onClick?: () => void; icon?: JSX.Element; - type?: "primary" | "danger"; + type?: ButtonType; text?: string; bordered?: boolean; shadow?: boolean; className?: string; title?: string; disabled?: boolean; + tabIndex?: number; + autoFocus?: boolean; }) { return (