Merge branch 'ChatGPTNextWeb:main' into main

This commit is contained in:
yinchangbo 2024-10-26 02:06:12 +08:00 committed by GitHub
commit aa2d79a14e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
156 changed files with 20036 additions and 3371 deletions

View File

@ -1,21 +1,20 @@
# Your openai api key. (required) # Your openai api key. (required)
OPENAI_API_KEY=sk-xxxx OPENAI_API_KEY=sk-xxxx
# Access password, separated by comma. (optional) # Access password, separated by comma. (optional)
CODE=your-password CODE=your-password
# You can start service behind a proxy # You can start service behind a proxy. (optional)
PROXY_URL=http://localhost:7890 PROXY_URL=http://localhost:7890
# (optional) # (optional)
# Default: Empty # Default: Empty
# Googel Gemini Pro API key, set if you want to use Google Gemini Pro API. # Google Gemini Pro API key, set if you want to use Google Gemini Pro API.
GOOGLE_API_KEY= GOOGLE_API_KEY=
# (optional) # (optional)
# Default: https://generativelanguage.googleapis.com/ # Default: https://generativelanguage.googleapis.com/
# Googel Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url. # Google Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
GOOGLE_URL= GOOGLE_URL=
# Override openai api request base url. (optional) # Override openai api request base url. (optional)
@ -47,6 +46,15 @@ ENABLE_BALANCE_QUERY=
# If you want to disable parse settings from url, set this value to 1. # If you want to disable parse settings from url, set this value to 1.
DISABLE_FAST_LINK= DISABLE_FAST_LINK=
# (optional)
# Default: Empty
# To control custom models, use + to add a custom model, use - to hide a model, use name=displayName to customize model name, separated by comma.
CUSTOM_MODELS=
# (optional)
# Default: Empty
# Change default model
DEFAULT_MODEL=
# anthropic claude Api Key.(optional) # anthropic claude Api Key.(optional)
ANTHROPIC_API_KEY= ANTHROPIC_API_KEY=
@ -54,10 +62,8 @@ ANTHROPIC_API_KEY=
### anthropic claude Api version. (optional) ### anthropic claude Api version. (optional)
ANTHROPIC_API_VERSION= ANTHROPIC_API_VERSION=
### anthropic claude Api url (optional) ### anthropic claude Api url (optional)
ANTHROPIC_URL= ANTHROPIC_URL=
### (optional) ### (optional)
WHITE_WEBDEV_ENDPOINTS= WHITE_WEBDAV_ENDPOINTS=

View File

@ -1,4 +1,7 @@
{ {
"extends": "next/core-web-vitals", "extends": "next/core-web-vitals",
"plugins": ["prettier"] "plugins": ["prettier", "unused-imports"],
"rules": {
"unused-imports/no-unused-imports": "warn"
}
} }

View File

@ -3,9 +3,7 @@ name: VercelPreviewDeployment
on: on:
pull_request_target: pull_request_target:
types: types:
- opened - review_requested
- synchronize
- reopened
env: env:
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }} VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
@ -49,7 +47,7 @@ jobs:
run: npm install --global vercel@latest run: npm install --global vercel@latest
- name: Cache dependencies - name: Cache dependencies
uses: actions/cache@v2 uses: actions/cache@v4
id: cache-npm id: cache-npm
with: with:
path: ~/.npm path: ~/.npm

39
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Run Tests
on:
push:
branches:
- main
tags:
- "!*"
pull_request:
types:
- review_requested
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: "yarn"
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node_modules-
- name: Install dependencies
run: yarn install
- name: Run Jest tests
run: yarn test:ci

2
.gitignore vendored
View File

@ -44,3 +44,5 @@ dev
*.key *.key
*.key.pub *.key.pub
masks.json

109
README.md
View File

@ -1,5 +1,8 @@
<div align="center"> <div align="center">
<img src="./docs/images/head-cover.png" alt="icon"/>
<a href='#企业版'>
<img src="./docs/images/ent.svg" alt="icon"/>
</a>
<h1 align="center">NextChat (ChatGPT Next Web)</h1> <h1 align="center">NextChat (ChatGPT Next Web)</h1>
@ -9,15 +12,18 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。 一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。
[![Saas][Saas-image]][saas-url]
[![Web][Web-image]][web-url] [![Web][Web-image]][web-url]
[![Windows][Windows-image]][download-url] [![Windows][Windows-image]][download-url]
[![MacOS][MacOS-image]][download-url] [![MacOS][MacOS-image]][download-url]
[![Linux][Linux-image]][download-url] [![Linux][Linux-image]][download-url]
[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Twitter](https://twitter.com/NextChatDev) [NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [Web App](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) [NextChatAI](https://nextchat.dev/chat) / [网页版](https://app.nextchat.dev) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
[saas-url]: https://nextchat.dev/chat?utm_source=readme
[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
[web-url]: https://app.nextchat.dev/ [web-url]: https://app.nextchat.dev/
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases [download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge [Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
@ -25,16 +31,40 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple [MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu [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%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/ZBUEFA) [<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
![cover](./docs/images/cover.png)
</div> </div>
## Enterprise Edition
Meeting Your Company's Privatization and Customization Deployment Requirements:
- **Brand Customization**: Tailored VI/UI to seamlessly align with your corporate brand image.
- **Resource Integration**: Unified configuration and management of dozens of AI resources by company administrators, ready for use by team members.
- **Permission Control**: Clearly defined member permissions, resource permissions, and knowledge base permissions, all controlled via a corporate-grade Admin Panel.
- **Knowledge Integration**: Combining your internal knowledge base with AI capabilities, making it more relevant to your company's specific business needs compared to general AI.
- **Security Auditing**: Automatically intercept sensitive inquiries and trace all historical conversation records, ensuring AI adherence to corporate information security standards.
- **Private Deployment**: Enterprise-level private deployment supporting various mainstream private cloud solutions, ensuring data security and privacy protection.
- **Continuous Updates**: Ongoing updates and upgrades in cutting-edge capabilities like multimodal AI, ensuring consistent innovation and advancement.
For enterprise inquiries, please contact: **business@nextchat.dev**
## 企业版
满足企业用户私有化部署和个性化定制需求:
- **品牌定制**:企业量身定制 VI/UI与企业品牌形象无缝契合
- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
企业版咨询: **business@nextchat.dev**
<img width="300" src="https://github.com/user-attachments/assets/3d4305ac-6e95-489e-884b-51d51db5f692">
## Features ## Features
- **Deploy for free with one-click** on Vercel in under 1 minute - **Deploy for free with one-click** on Vercel in under 1 minute
@ -49,6 +79,12 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
- Automatically compresses chat history to support long conversations while also saving your tokens - Automatically compresses chat history to support long conversations while also saving your tokens
- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia - I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
<div align="center">
![主界面](./docs/images/cover.png)
</div>
## Roadmap ## Roadmap
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138) - [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
@ -57,10 +93,16 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741) - [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
- [x] Desktop App with tauri - [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. - [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) - [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
- [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [ ] local knowledge base
## What's New ## What's New
- 🚀 v2.15.4 The Application supports using Tauri fetch LLM API, MORE SECURITY! [#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 Now supports Artifacts & SD
- 🚀 v2.10.1 support Google Gemini Pro model. - 🚀 v2.10.1 support Google Gemini Pro model.
- 🚀 v2.9.11 you can use azure endpoint now. - 🚀 v2.9.11 you can use azure endpoint now.
- 🚀 v2.8 now we have a client that runs across all platforms! - 🚀 v2.8 now we have a client that runs across all platforms!
@ -89,15 +131,22 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
- [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741) - [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
- [x] 使用 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) - [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) - [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
- [x] 插件机制,支持`联网搜索`、`计算器`、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [ ] 本地知识库
## 最新动态 ## 最新动态
- 🚀 v2.15.4 客户端支持Tauri本地直接调用大模型API更安全[#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。 - 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
- 💡 想要更方便地随时随地使用本项目可以试下这款桌面插件https://github.com/mushan0x0/AI0x0.com - 💡 想要更方便地随时随地使用本项目可以试下这款桌面插件https://github.com/mushan0x0/AI0x0.com
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
## Get Started ## Get Started
@ -128,7 +177,7 @@ We recommend that you follow the steps below to re-deploy:
### Enable Automatic Updates ### Enable Automatic Updates
> If you encounter a failure of Upstream Sync execution, please manually sync fork once. > If you encounter a failure of Upstream Sync execution, please [manually update code](./README.md#manually-updating-code).
After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour: After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
@ -180,8 +229,7 @@ Specify OpenAI organization ID.
### `AZURE_URL` (optional) ### `AZURE_URL` (optional)
> Example: https://{azure-resource-url}/openai/deployments/{deploy-name} > Example: https://{azure-resource-url}/openai
> if you config deployment name in `CUSTOM_MODELS`, you can remove `{deploy-name}` in `AZURE_URL`
Azure deploy url. Azure deploy url.
@ -241,6 +289,18 @@ Alibaba Cloud Api Key.
Alibaba Cloud Api Url. Alibaba Cloud Api Url.
### `IFLYTEK_URL` (Optional)
iflytek Api Url.
### `IFLYTEK_API_KEY` (Optional)
iflytek Api Key.
### `IFLYTEK_API_SECRET` (Optional)
iflytek Api Secret.
### `HIDE_USER_API_KEY` (optional) ### `HIDE_USER_API_KEY` (optional)
> Default: Empty > Default: Empty
@ -274,8 +334,9 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model
User `-all` to disable all default models, `+all` to enable all default models. User `-all` to disable all default models, `+all` to enable all default models.
For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name. For Azure: use `modelName@Azure=deploymentName` to customize model name and deployment name.
> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list. > Example: `+gpt-3.5-turbo@Azure=gpt35` will show option `gpt35(Azure)` in model list.
> If you only can use Azure model, `-all,+gpt-3.5-turbo@Azure=gpt35` will `gpt35(Azure)` the only option in model list.
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name. For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list. > Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
@ -284,7 +345,7 @@ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name
Change default model Change default model
### `WHITE_WEBDEV_ENDPOINTS` (optional) ### `WHITE_WEBDAV_ENDPOINTS` (optional)
You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format
- Each address must be a complete endpoint - Each address must be a complete endpoint
@ -295,6 +356,14 @@ You can use this option if you want to increase the number of webdav service add
Customize the default template used to initialize the User Input Preprocessing configuration item in Settings. Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
### `STABILITY_API_KEY` (optional)
Stability API key.
### `STABILITY_URL` (optional)
Customize Stability API url.
## Requirements ## Requirements
NodeJS >= 18, Docker >= 20 NodeJS >= 18, Docker >= 20

View File

@ -1,22 +1,34 @@
<div align="center"> <div align="center">
<img src="./docs/images/icon.svg" alt="预览"/>
<a href='#企业版'>
<img src="./docs/images/ent.svg" alt="icon"/>
</a>
<h1 align="center">NextChat</h1> <h1 align="center">NextChat</h1>
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。 一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) [NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
[![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) [<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/ZBUEFA)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
![主界面](./docs/images/cover.png)
</div> </div>
## 企业版
满足您公司私有化部署和定制需求
- **品牌定制**:企业量身定制 VI/UI与企业品牌形象无缝契合
- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
企业版咨询: **business@nextchat.dev**
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
## 开始使用 ## 开始使用
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys); 1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
@ -25,6 +37,12 @@
3. 部署完毕后,即可开始使用; 3. 部署完毕后,即可开始使用;
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain)Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。 4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain)Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
<div align="center">
![主界面](./docs/images/cover.png)
</div>
## 保持更新 ## 保持更新
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。 如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
@ -36,7 +54,7 @@
### 打开自动更新 ### 打开自动更新
> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次! > 如果你遇到了 Upstream Sync 执行错误,请[手动 Sync Fork 一次](./README_CN.md#手动更新代码)
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows并启用 Upstream Sync Action启用之后即可开启每小时定时自动更新 当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows并启用 Upstream Sync Action启用之后即可开启每小时定时自动更新
@ -94,8 +112,7 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
### `AZURE_URL` (可选) ### `AZURE_URL` (可选)
> 形如https://{azure-resource-url}/openai/deployments/{deploy-name} > 形如https://{azure-resource-url}/openai
> 如果你已经在`CUSTOM_MODELS`中参考`displayName`的方式配置了{deploy-name},那么可以从`AZURE_URL`中移除`{deploy-name}`
Azure 部署地址。 Azure 部署地址。
@ -155,6 +172,20 @@ ByteDance Api Url.
阿里云千问Api Url. 阿里云千问Api Url.
### `IFLYTEK_URL` (可选)
讯飞星火Api Url.
### `IFLYTEK_API_KEY` (可选)
讯飞星火Api Key.
### `IFLYTEK_API_SECRET` (可选)
讯飞星火Api Secret.
### `HIDE_USER_API_KEY` (可选) ### `HIDE_USER_API_KEY` (可选)
如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。 如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。
@ -171,7 +202,7 @@ ByteDance Api Url.
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。 如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
### `WHITE_WEBDEV_ENDPOINTS` (可选) ### `WHITE_WEBDAV_ENDPOINTS` (可选)
如果你想增加允许访问的webdav服务地址可以使用该选项格式要求 如果你想增加允许访问的webdav服务地址可以使用该选项格式要求
- 每一个地址必须是一个完整的 endpoint - 每一个地址必须是一个完整的 endpoint
@ -185,8 +216,9 @@ ByteDance Api Url.
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
在Azure的模式下支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name) 在Azure的模式下支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项 > 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
> 如果你只能使用Azure模式那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
在ByteDance的模式下支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name) 在ByteDance的模式下支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项 > 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
@ -200,6 +232,15 @@ ByteDance Api Url.
自定义默认的 template用于初始化『设置』中的『用户输入预处理』配置项 自定义默认的 template用于初始化『设置』中的『用户输入预处理』配置项
### `STABILITY_API_KEY` (optional)
Stability API密钥
### `STABILITY_URL` (optional)
自定义的Stability API请求地址
## 开发 ## 开发
点击下方按钮,开始二次开发: 点击下方按钮,开始二次开发:

310
README_JA.md Normal file
View File

@ -0,0 +1,310 @@
<div align="center">
<img src="./docs/images/ent.svg" alt="プレビュー"/>
<h1 align="center">NextChat</h1>
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
</div>
## 企業版
あなたの会社のプライベートデプロイとカスタマイズのニーズに応える
- **ブランドカスタマイズ**:企業向けに特別に設計された VI/UI、企業ブランドイメージとシームレスにマッチ
- **リソース統合**企業管理者が数十種類のAIリソースを統一管理、チームメンバーはすぐに使用可能
- **権限管理**メンバーの権限、リソースの権限、ナレッジベースの権限を明確にし、企業レベルのAdmin Panelで統一管理
- **知識の統合**企業内部のナレッジベースとAI機能を結びつけ、汎用AIよりも企業自身の業務ニーズに近づける
- **セキュリティ監査**機密質問を自動的にブロックし、すべての履歴対話を追跡可能にし、AIも企業の情報セキュリティ基準に従わせる
- **プライベートデプロイ**:企業レベルのプライベートデプロイ、主要なプライベートクラウドデプロイをサポートし、データのセキュリティとプライバシーを保護
- **継続的な更新**:マルチモーダル、エージェントなどの最先端機能を継続的に更新し、常に最新であり続ける
企業版のお問い合わせ: **business@nextchat.dev**
## 始めに
1. [OpenAI API Key](https://platform.openai.com/account/api-keys)を準備する;
2. 右側のボタンをクリックしてデプロイを開始:
[![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&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) 、GitHubアカウントで直接ログインし、環境変数ページにAPI Keyと[ページアクセスパスワード](#設定ページアクセスパスワード) CODEを入力してください;
3. デプロイが完了したら、すぐに使用を開始できます;
4. (オプション)[カスタムドメインをバインド](https://vercel.com/docs/concepts/projects/domains/add-a-domain)Vercelが割り当てたドメインDNSは一部の地域で汚染されているため、カスタムドメインをバインドすると直接接続できます。
<div align="center">
![メインインターフェース](./docs/images/cover.png)
</div>
## 更新を維持する
もし上記の手順に従ってワンクリックでプロジェクトをデプロイした場合、「更新があります」というメッセージが常に表示されることがあります。これは、Vercel がデフォルトで新しいプロジェクトを作成するためで、本プロジェクトを fork していないことが原因です。そのため、正しく更新を検出できません。
以下の手順に従って再デプロイすることをお勧めします:
- 元のリポジトリを削除する
- ページ右上の fork ボタンを使って、本プロジェクトを fork する
- Vercel で再度選択してデプロイする、[詳細な手順はこちらを参照してください](./docs/vercel-ja.md)。
### 自動更新を開く
> Upstream Sync の実行エラーが発生した場合は、[手動で Sync Fork](./README_JA.md#手動でコードを更新する) してください!
プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
![自動更新](./docs/images/enable-actions.jpg)
![自動更新を有効にする](./docs/images/enable-actions-sync.jpg)
### 手動でコードを更新する
手動で即座に更新したい場合は、[GitHub のドキュメント](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)を参照して、fork したプロジェクトを上流のコードと同期する方法を確認してください。
このプロジェクトをスターまたはウォッチしたり、作者をフォローすることで、新機能の更新通知をすぐに受け取ることができます。
## ページアクセスパスワードを設定する
> パスワードを設定すると、ユーザーは設定ページでアクセスコードを手動で入力しない限り、通常のチャットができず、未承認の状態であることを示すメッセージが表示されます。
> **警告**パスワードの桁数は十分に長く設定してください。7桁以上が望ましいです。さもないと、[ブルートフォース攻撃を受ける可能性があります](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
このプロジェクトは限られた権限管理機能を提供しています。Vercel プロジェクトのコントロールパネルで、環境変数ページに `CODE` という名前の環境変数を追加し、値をカンマで区切ったカスタムパスワードに設定してください:
```
code1,code2,code3
```
この環境変数を追加または変更した後、**プロジェクトを再デプロイ**して変更を有効にしてください。
## 環境変数
> 本プロジェクトのほとんどの設定は環境変数で行います。チュートリアル:[Vercel の環境変数を変更する方法](./docs/vercel-ja.md)。
### `OPENAI_API_KEY` (必須)
OpenAI の API キー。OpenAI アカウントページで申請したキーをカンマで区切って複数設定できます。これにより、ランダムにキーが選択されます。
### `CODE` (オプション)
アクセスパスワード。カンマで区切って複数設定可能。
**警告**:この項目を設定しないと、誰でもデプロイしたウェブサイトを利用でき、トークンが急速に消耗する可能性があるため、設定をお勧めします。
### `BASE_URL` (オプション)
> デフォルト: `https://api.openai.com`
> 例: `http://your-openai-proxy.com`
OpenAI API のプロキシ URL。手動で OpenAI API のプロキシを設定している場合はこのオプションを設定してください。
> SSL 証明書の問題がある場合は、`BASE_URL` のプロトコルを http に設定してください。
### `OPENAI_ORG_ID` (オプション)
OpenAI の組織 ID を指定します。
### `AZURE_URL` (オプション)
> 形式: https://{azure-resource-url}/openai/deployments/{deploy-name}
> `CUSTOM_MODELS``displayName` 形式で {deploy-name} を設定した場合、`AZURE_URL` から {deploy-name} を省略できます。
Azure のデプロイ URL。
### `AZURE_API_KEY` (オプション)
Azure の API キー。
### `AZURE_API_VERSION` (オプション)
Azure API バージョン。[Azure ドキュメント](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)で確認できます。
### `GOOGLE_API_KEY` (オプション)
Google Gemini Pro API キー。
### `GOOGLE_URL` (オプション)
Google Gemini Pro API の URL。
### `ANTHROPIC_API_KEY` (オプション)
Anthropic Claude API キー。
### `ANTHROPIC_API_VERSION` (オプション)
Anthropic Claude API バージョン。
### `ANTHROPIC_URL` (オプション)
Anthropic Claude API の URL。
### `BAIDU_API_KEY` (オプション)
Baidu API キー。
### `BAIDU_SECRET_KEY` (オプション)
Baidu シークレットキー。
### `BAIDU_URL` (オプション)
Baidu API の URL。
### `BYTEDANCE_API_KEY` (オプション)
ByteDance API キー。
### `BYTEDANCE_URL` (オプション)
ByteDance API の URL。
### `ALIBABA_API_KEY` (オプション)
アリババ千问API キー。
### `ALIBABA_URL` (オプション)
アリババ千问API の URL。
### `HIDE_USER_API_KEY` (オプション)
ユーザーが API キーを入力できないようにしたい場合は、この環境変数を 1 に設定します。
### `DISABLE_GPT4` (オプション)
ユーザーが GPT-4 を使用できないようにしたい場合は、この環境変数を 1 に設定します。
### `ENABLE_BALANCE_QUERY` (オプション)
バランスクエリ機能を有効にしたい場合は、この環境変数を 1 に設定します。
### `DISABLE_FAST_LINK` (オプション)
リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
### `WHITE_WEBDAV_ENDPOINTS` (オプション)
アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
- 各アドレスは完全なエンドポイントでなければなりません。
> `https://xxxx/xxx`
- 複数のアドレスは `,` で接続します。
### `CUSTOM_MODELS` (オプション)
> 例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` は `qwen-7b-chat``glm-6b` をモデルリストに追加し、`gpt-3.5-turbo` を削除し、`gpt-4-1106-preview` のモデル名を `gpt-4-turbo` として表示します。
> すべてのモデルを無効にし、特定のモデルを有効にしたい場合は、`-all,+gpt-3.5-turbo` を使用します。これは `gpt-3.5-turbo` のみを有効にすることを意味します。
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
Azure モードでは、`modelName@Azure=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。
> 例:`+gpt-3.5-turbo@Azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
### `DEFAULT_MODEL` (オプション)
デフォルトのモデルを変更します。
### `DEFAULT_INPUT_TEMPLATE` (オプション)
『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
## 開発
下のボタンをクリックして二次開発を開始してください:
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
コードを書く前に、プロジェクトのルートディレクトリに `.env.local` ファイルを新規作成し、環境変数を記入します:
```
OPENAI_API_KEY=<your api key here>
```
### ローカル開発
1. Node.js 18 と Yarn をインストールします。具体的な方法は ChatGPT にお尋ねください。
2. `yarn install && yarn dev` を実行します。⚠️ 注意:このコマンドはローカル開発用であり、デプロイには使用しないでください。
3. ローカルでデプロイしたい場合は、`yarn install && yarn build && yarn start` コマンドを使用してください。プロセスを守るために pm2 を使用することもできます。詳細は ChatGPT にお尋ねください。
## デプロイ
### コンテナデプロイ(推奨)
> Docker バージョンは 20 以上が必要です。それ以下だとイメージが見つからないというエラーが出ます。
> ⚠️ 注意Docker バージョンは最新バージョンより 12 日遅れることが多いため、デプロイ後に「更新があります」の通知が出続けることがありますが、正常です。
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY=sk-xxxx \
-e CODE=ページアクセスパスワード \
yidadaa/chatgpt-next-web
```
プロキシを指定することもできます:
```shell
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY=sk-xxxx \
-e CODE=ページアクセスパスワード \
--net=host \
-e PROXY_URL=http://127.0.0.1:7890 \
yidadaa/chatgpt-next-web
```
ローカルプロキシがアカウントとパスワードを必要とする場合は、以下を使用できます:
```shell
-e PROXY_URL="http://127.0.0.1:7890 user password"
```
他の環境変数を指定する必要がある場合は、上記のコマンドに `-e 環境変数=環境変数値` を追加して指定してください。
### ローカルデプロイ
コンソールで以下のコマンドを実行します:
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
⚠️ 注意インストール中に問題が発生した場合は、Docker を使用してデプロイしてください。
## 謝辞
### 寄付者
> 英語版をご覧ください。
### 貢献者
[プロジェクトの貢献者リストはこちら](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
### 関連プロジェクト
- [one-api](https://github.com/songquanpeng/one-api): 一つのプラットフォームで大規模モデルのクォータ管理を提供し、市場に出回っているすべての主要な大規模言語モデルをサポートします。
## オープンソースライセンス
[MIT](https://opensource.org/license/mit/)

View File

@ -0,0 +1,73 @@
import { ApiPath } from "@/app/constant";
import { NextRequest } from "next/server";
import { handle as openaiHandler } from "../../openai";
import { handle as azureHandler } from "../../azure";
import { handle as googleHandler } from "../../google";
import { handle as anthropicHandler } from "../../anthropic";
import { handle as baiduHandler } from "../../baidu";
import { handle as bytedanceHandler } from "../../bytedance";
import { handle as alibabaHandler } from "../../alibaba";
import { handle as moonshotHandler } from "../../moonshot";
import { handle as stabilityHandler } from "../../stability";
import { handle as iflytekHandler } from "../../iflytek";
import { handle as xaiHandler } from "../../xai";
import { handle as proxyHandler } from "../../proxy";
async function handle(
req: NextRequest,
{ params }: { params: { provider: string; path: string[] } },
) {
const apiPath = `/api/${params.provider}`;
console.log(`[${params.provider} Route] params `, params);
switch (apiPath) {
case ApiPath.Azure:
return azureHandler(req, { params });
case ApiPath.Google:
return googleHandler(req, { params });
case ApiPath.Anthropic:
return anthropicHandler(req, { params });
case ApiPath.Baidu:
return baiduHandler(req, { params });
case ApiPath.ByteDance:
return bytedanceHandler(req, { params });
case ApiPath.Alibaba:
return alibabaHandler(req, { params });
// case ApiPath.Tencent: using "/api/tencent"
case ApiPath.Moonshot:
return moonshotHandler(req, { params });
case ApiPath.Stability:
return stabilityHandler(req, { params });
case ApiPath.Iflytek:
return iflytekHandler(req, { params });
case ApiPath.XAI:
return xaiHandler(req, { params });
case ApiPath.OpenAI:
return openaiHandler(req, { params });
default:
return proxyHandler(req, { params });
}
}
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];

View File

@ -1,6 +1,5 @@
import { getServerSideConfig } from "@/app/config/server"; import { getServerSideConfig } from "@/app/config/server";
import { import {
Alibaba,
ALIBABA_BASE_URL, ALIBABA_BASE_URL,
ApiPath, ApiPath,
ModelProvider, ModelProvider,
@ -10,11 +9,10 @@ import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth"; import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model"; import { isModelAvailableInServer } from "@/app/utils/model";
import type { RequestPayload } from "@/app/client/platforms/openai";
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
async function handle( export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
@ -40,30 +38,6 @@ async function handle(
} }
} }
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];
async function request(req: NextRequest) { async function request(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();

View File

@ -3,19 +3,18 @@ import {
ANTHROPIC_BASE_URL, ANTHROPIC_BASE_URL,
Anthropic, Anthropic,
ApiPath, ApiPath,
DEFAULT_MODELS,
ServiceProvider, ServiceProvider,
ModelProvider, ModelProvider,
} from "@/app/constant"; } from "@/app/constant";
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth"; import { auth } from "./auth";
import { isModelAvailableInServer } from "@/app/utils/model"; import { isModelAvailableInServer } from "@/app/utils/model";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]); const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
async function handle( export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
@ -56,30 +55,6 @@ async function handle(
} }
} }
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
async function request(req: NextRequest) { async function request(req: NextRequest) {
@ -122,6 +97,7 @@ async function request(req: NextRequest) {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Cache-Control": "no-store", "Cache-Control": "no-store",
"anthropic-dangerous-direct-browser-access": "true",
[authHeaderName]: authValue, [authHeaderName]: authValue,
"anthropic-version": "anthropic-version":
req.headers.get("anthropic-version") || req.headers.get("anthropic-version") ||

View File

@ -0,0 +1,73 @@
import md5 from "spark-md5";
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "@/app/config/server";
async function handle(req: NextRequest, res: NextResponse) {
const serverConfig = getServerSideConfig();
const storeUrl = () =>
`https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`;
const storeHeaders = () => ({
Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
});
if (req.method === "POST") {
const clonedBody = await req.text();
const hashedCode = md5.hash(clonedBody).trim();
const body: {
key: string;
value: string;
expiration_ttl?: number;
} = {
key: hashedCode,
value: clonedBody,
};
try {
const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
if (ttl > 60) {
body["expiration_ttl"] = ttl;
}
} catch (e) {
console.error(e);
}
const res = await fetch(`${storeUrl()}/bulk`, {
headers: {
...storeHeaders(),
"Content-Type": "application/json",
},
method: "PUT",
body: JSON.stringify([body]),
});
const result = await res.json();
console.log("save data", result);
if (result?.success) {
return NextResponse.json(
{ code: 0, id: hashedCode, result },
{ status: res.status },
);
}
return NextResponse.json(
{ error: true, msg: "Save data error" },
{ status: 400 },
);
}
if (req.method === "GET") {
const id = req?.nextUrl?.searchParams?.get("id");
const res = await fetch(`${storeUrl()}/values/${id}`, {
headers: storeHeaders(),
method: "GET",
});
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: res.headers,
});
}
return NextResponse.json(
{ error: true, msg: "Invalid request" },
{ status: 400 },
);
}
export const POST = handle;
export const GET = handle;
export const runtime = "edge";

View File

@ -67,6 +67,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
let systemApiKey: string | undefined; let systemApiKey: string | undefined;
switch (modelProvider) { switch (modelProvider) {
case ModelProvider.Stability:
systemApiKey = serverConfig.stabilityApiKey;
break;
case ModelProvider.GeminiPro: case ModelProvider.GeminiPro:
systemApiKey = serverConfig.googleApiKey; systemApiKey = serverConfig.googleApiKey;
break; break;
@ -82,6 +85,16 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
case ModelProvider.Qwen: case ModelProvider.Qwen:
systemApiKey = serverConfig.alibabaApiKey; systemApiKey = serverConfig.alibabaApiKey;
break; break;
case ModelProvider.Moonshot:
systemApiKey = serverConfig.moonshotApiKey;
break;
case ModelProvider.Iflytek:
systemApiKey =
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
break;
case ModelProvider.XAI:
systemApiKey = serverConfig.xaiApiKey;
break;
case ModelProvider.GPT: case ModelProvider.GPT:
default: default:
if (req.nextUrl.pathname.includes("azure/deployments")) { if (req.nextUrl.pathname.includes("azure/deployments")) {

View File

@ -1,11 +1,10 @@
import { getServerSideConfig } from "@/app/config/server";
import { ModelProvider } from "@/app/constant"; import { ModelProvider } from "@/app/constant";
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth"; import { auth } from "./auth";
import { requestOpenai } from "../../common"; import { requestOpenai } from "./common";
async function handle( export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
@ -31,27 +30,3 @@ async function handle(
return NextResponse.json(prettyObject(e)); return NextResponse.json(prettyObject(e));
} }
} }
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];

View File

@ -3,7 +3,6 @@ import {
BAIDU_BASE_URL, BAIDU_BASE_URL,
ApiPath, ApiPath,
ModelProvider, ModelProvider,
BAIDU_OATUH_URL,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; } from "@/app/constant";
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
@ -14,7 +13,7 @@ import { getAccessToken } from "@/app/utils/baidu";
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
async function handle( export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
@ -52,30 +51,6 @@ async function handle(
} }
} }
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];
async function request(req: NextRequest) { async function request(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();

View File

@ -12,7 +12,7 @@ import { isModelAvailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
async function handle( export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
@ -38,30 +38,6 @@ async function handle(
} }
} }
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];
async function request(req: NextRequest) { async function request(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();

View File

@ -1,11 +1,6 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../config/server"; import { getServerSideConfig } from "../config/server";
import { import { OPENAI_BASE_URL, ServiceProvider } from "../constant";
DEFAULT_MODELS,
OPENAI_BASE_URL,
GEMINI_BASE_URL,
ServiceProvider,
} from "../constant";
import { isModelAvailableInServer } from "../utils/model"; import { isModelAvailableInServer } from "../utils/model";
import { cloudflareAIGatewayUrl } from "../utils/cloudflare"; import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
@ -32,10 +27,7 @@ export async function requestOpenai(req: NextRequest) {
authHeaderName = "Authorization"; authHeaderName = "Authorization";
} }
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");
"/api/openai/",
"",
);
let baseUrl = let baseUrl =
(isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL; (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;

View File

@ -1,11 +1,14 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth"; import { auth } from "./auth";
import { getServerSideConfig } from "@/app/config/server"; import { getServerSideConfig } from "@/app/config/server";
import { GEMINI_BASE_URL, Google, ModelProvider } from "@/app/constant"; import { ApiPath, GEMINI_BASE_URL, ModelProvider } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
async function handle( const serverConfig = getServerSideConfig();
export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { provider: string; path: string[] } },
) { ) {
console.log("[Google Route] params ", params); console.log("[Google Route] params ", params);
@ -13,32 +16,6 @@ async function handle(
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: "OK" }, { status: 200 });
} }
const controller = new AbortController();
const serverConfig = getServerSideConfig();
let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
let path = `${req.nextUrl.pathname}`.replaceAll("/api/google/", "");
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const authResult = auth(req, ModelProvider.GeminiPro); const authResult = auth(req, ModelProvider.GeminiPro);
if (authResult.error) { if (authResult.error) {
return NextResponse.json(authResult, { return NextResponse.json(authResult, {
@ -46,12 +23,13 @@ async function handle(
}); });
} }
const bearToken = req.headers.get("Authorization") ?? ""; const bearToken =
req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || "";
const token = bearToken.trim().replaceAll("Bearer ", "").trim(); const token = bearToken.trim().replaceAll("Bearer ", "").trim();
const key = token ? token : serverConfig.googleApiKey; const apiKey = token ? token : serverConfig.googleApiKey;
if (!key) { if (!apiKey) {
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
@ -62,14 +40,70 @@ async function handle(
}, },
); );
} }
try {
const response = await request(req, apiKey);
return response;
} catch (e) {
console.error("[Google] ", e);
return NextResponse.json(prettyObject(e));
}
}
const fetchUrl = `${baseUrl}/${path}?key=${key}${ export const GET = handle;
req?.nextUrl?.searchParams?.get("alt") == "sse" ? "&alt=sse" : "" export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"bom1",
"cle1",
"cpt1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];
async function request(req: NextRequest, apiKey: string) {
const controller = new AbortController();
let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, "");
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const fetchUrl = `${baseUrl}${path}${
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : ""
}`; }`;
console.log("[Fetch Url] ", fetchUrl);
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Cache-Control": "no-store", "Cache-Control": "no-store",
"x-goog-api-key":
req.headers.get("x-goog-api-key") ||
(req.headers.get("Authorization") ?? "").replace("Bearer ", ""),
}, },
method: req.method, method: req.method,
body: req.body, body: req.body,
@ -97,22 +131,3 @@ async function handle(
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
} }
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"bom1",
"cle1",
"cpt1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];

129
app/api/iflytek.ts Normal file
View File

@ -0,0 +1,129 @@
import { getServerSideConfig } from "@/app/config/server";
import {
IFLYTEK_BASE_URL,
ApiPath,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
// iflytek
const serverConfig = getServerSideConfig();
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Iflytek Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const authResult = auth(req, ModelProvider.Iflytek);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const response = await request(req);
return response;
} catch (e) {
console.error("[Iflytek] ", e);
return NextResponse.json(prettyObject(e));
}
}
async function request(req: NextRequest) {
const controller = new AbortController();
// iflytek use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, "");
let baseUrl = serverConfig.iflytekUrl || IFLYTEK_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
},
method: req.method,
body: req.body,
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
// try to refuse some request to some models
if (serverConfig.customModels && req.body) {
try {
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody) as { model?: string };
// not undefined and is false
if (
isModelAvailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Iflytek as string,
)
) {
return NextResponse.json(
{
error: true,
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,
},
);
}
} catch (e) {
console.error(`[Iflytek] filter`, e);
}
}
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}

128
app/api/moonshot.ts Normal file
View File

@ -0,0 +1,128 @@
import { getServerSideConfig } from "@/app/config/server";
import {
MOONSHOT_BASE_URL,
ApiPath,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig();
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Moonshot Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const authResult = auth(req, ModelProvider.Moonshot);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const response = await request(req);
return response;
} catch (e) {
console.error("[Moonshot] ", e);
return NextResponse.json(prettyObject(e));
}
}
async function request(req: NextRequest) {
const controller = new AbortController();
// alibaba use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Moonshot, "");
let baseUrl = serverConfig.moonshotUrl || MOONSHOT_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
},
method: req.method,
body: req.body,
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
// #1815 try to refuse some request to some models
if (serverConfig.customModels && req.body) {
try {
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody) as { model?: string };
// not undefined and is false
if (
isModelAvailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Moonshot as string,
)
) {
return NextResponse.json(
{
error: true,
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,
},
);
}
} catch (e) {
console.error(`[Moonshot] filter`, e);
}
}
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}

View File

@ -3,24 +3,26 @@ import { getServerSideConfig } from "@/app/config/server";
import { ModelProvider, OpenaiPath } from "@/app/constant"; import { ModelProvider, OpenaiPath } from "@/app/constant";
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth"; import { auth } from "./auth";
import { requestOpenai } from "../../common"; import { requestOpenai } from "./common";
const ALLOWD_PATH = new Set(Object.values(OpenaiPath)); const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
function getModels(remoteModelRes: OpenAIListModelResponse) { function getModels(remoteModelRes: OpenAIListModelResponse) {
const config = getServerSideConfig(); const config = getServerSideConfig();
if (config.disableGPT4) { if (config.disableGPT4) {
remoteModelRes.data = remoteModelRes.data.filter( remoteModelRes.data = remoteModelRes.data.filter(
(m) => !m.id.startsWith("gpt-4"), (m) =>
!(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o")) ||
m.id.startsWith("gpt-4o-mini"),
); );
} }
return remoteModelRes; return remoteModelRes;
} }
async function handle( export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
@ -32,7 +34,7 @@ async function handle(
const subpath = params.path.join("/"); const subpath = params.path.join("/");
if (!ALLOWD_PATH.has(subpath)) { if (!ALLOWED_PATH.has(subpath)) {
console.log("[OpenAI Route] forbidden path ", subpath); console.log("[OpenAI Route] forbidden path ", subpath);
return NextResponse.json( return NextResponse.json(
{ {
@ -70,27 +72,3 @@ async function handle(
return NextResponse.json(prettyObject(e)); return NextResponse.json(prettyObject(e));
} }
} }
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];

75
app/api/proxy.ts Normal file
View File

@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from "next/server";
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Proxy Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
// remove path params from searchParams
req.nextUrl.searchParams.delete("path");
req.nextUrl.searchParams.delete("provider");
const subpath = params.path.join("/");
const fetchUrl = `${req.headers.get(
"x-base-url",
)}/${subpath}?${req.nextUrl.searchParams.toString()}`;
const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
const headers = new Headers(
Array.from(req.headers.entries()).filter((item) => {
if (
item[0].indexOf("x-") > -1 ||
item[0].indexOf("sec-") > -1 ||
skipHeaders.includes(item[0])
) {
return false;
}
return true;
}),
);
const controller = new AbortController();
const fetchOptions: RequestInit = {
headers,
method: req.method,
body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
// So if the streaming is disabled, we need to remove the content-encoding header
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
// The browser will try to decode the response with brotli and fail
newHeaders.delete("content-encoding");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}

99
app/api/stability.ts Normal file
View File

@ -0,0 +1,99 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "@/app/config/server";
import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant";
import { auth } from "@/app/api/auth";
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Stability] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const controller = new AbortController();
const serverConfig = getServerSideConfig();
let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", "");
console.log("[Stability Proxy] ", path);
console.log("[Stability Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const authResult = auth(req, ModelProvider.Stability);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
const bearToken = req.headers.get("Authorization") ?? "";
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
const key = token ? token : serverConfig.stabilityApiKey;
if (!key) {
return NextResponse.json(
{
error: true,
message: `missing STABILITY_API_KEY in server env vars`,
},
{
status: 401,
},
);
}
const fetchUrl = `${baseUrl}/${path}`;
console.log("[Stability Url] ", fetchUrl);
const fetchOptions: RequestInit = {
headers: {
"Content-Type": req.headers.get("Content-Type") || "multipart/form-data",
Accept: req.headers.get("Accept") || "application/json",
Authorization: `Bearer ${key}`,
},
method: req.method,
body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}

117
app/api/tencent/route.ts Normal file
View File

@ -0,0 +1,117 @@
import { getServerSideConfig } from "@/app/config/server";
import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { getHeader } from "@/app/utils/tencent";
const serverConfig = getServerSideConfig();
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Tencent Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const authResult = auth(req, ModelProvider.Hunyuan);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const response = await request(req);
return response;
} catch (e) {
console.error("[Tencent] ", e);
return NextResponse.json(prettyObject(e));
}
}
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];
async function request(req: NextRequest) {
const controller = new AbortController();
let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const fetchUrl = baseUrl;
const body = await req.text();
const headers = await getHeader(
body,
serverConfig.tencentSecretId as string,
serverConfig.tencentSecretKey as string,
);
const fetchOptions: RequestInit = {
headers,
method: req.method,
body,
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}

View File

@ -6,7 +6,7 @@ const config = getServerSideConfig();
const mergedAllowedWebDavEndpoints = [ const mergedAllowedWebDavEndpoints = [
...internalAllowedWebDavEndpoints, ...internalAllowedWebDavEndpoints,
...config.allowedWebDevEndpoints, ...config.allowedWebDavEndpoints,
].filter((domain) => Boolean(domain.trim())); ].filter((domain) => Boolean(domain.trim()));
const normalizeUrl = (url: string) => { const normalizeUrl = (url: string) => {
@ -29,6 +29,7 @@ async function handle(
const requestUrl = new URL(req.url); const requestUrl = new URL(req.url);
let endpoint = requestUrl.searchParams.get("endpoint"); let endpoint = requestUrl.searchParams.get("endpoint");
let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method;
// Validate the endpoint to prevent potential SSRF attacks // Validate the endpoint to prevent potential SSRF attacks
if ( if (
@ -37,9 +38,13 @@ async function handle(
const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint); const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
const normalizedEndpoint = normalizeUrl(endpoint as string); const normalizedEndpoint = normalizeUrl(endpoint as string);
return normalizedEndpoint && return (
normalizedEndpoint &&
normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname && normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
normalizedEndpoint.pathname.startsWith(normalizedAllowedEndpoint.pathname); normalizedEndpoint.pathname.startsWith(
normalizedAllowedEndpoint.pathname,
)
);
}) })
) { ) {
return NextResponse.json( return NextResponse.json(
@ -61,7 +66,11 @@ async function handle(
const targetPath = `${endpoint}${endpointPath}`; const targetPath = `${endpoint}${endpointPath}`;
// only allow MKCOL, GET, PUT // only allow MKCOL, GET, PUT
if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") { if (
proxy_method !== "MKCOL" &&
proxy_method !== "GET" &&
proxy_method !== "PUT"
) {
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
@ -74,7 +83,7 @@ async function handle(
} }
// for MKCOL request, only allow request ${folder} // for MKCOL request, only allow request ${folder}
if (req.method === "MKCOL" && !targetPath.endsWith(folder)) { if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) {
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
@ -87,7 +96,7 @@ async function handle(
} }
// for GET request, only allow request ending with fileName // for GET request, only allow request ending with fileName
if (req.method === "GET" && !targetPath.endsWith(fileName)) { if (proxy_method === "GET" && !targetPath.endsWith(fileName)) {
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
@ -100,7 +109,7 @@ async function handle(
} }
// for PUT request, only allow request ending with fileName // for PUT request, only allow request ending with fileName
if (req.method === "PUT" && !targetPath.endsWith(fileName)) { if (proxy_method === "PUT" && !targetPath.endsWith(fileName)) {
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
@ -114,7 +123,7 @@ async function handle(
const targetUrl = targetPath; const targetUrl = targetPath;
const method = req.method; const method = proxy_method || req.method;
const shouldNotHaveBody = ["get", "head"].includes( const shouldNotHaveBody = ["get", "head"].includes(
method?.toLowerCase() ?? "", method?.toLowerCase() ?? "",
); );
@ -139,7 +148,7 @@ async function handle(
"[Any Proxy]", "[Any Proxy]",
targetUrl, targetUrl,
{ {
method: req.method, method: method,
}, },
{ {
status: fetchResult?.status, status: fetchResult?.status,

128
app/api/xai.ts Normal file
View File

@ -0,0 +1,128 @@
import { getServerSideConfig } from "@/app/config/server";
import {
XAI_BASE_URL,
ApiPath,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig();
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[XAI Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const authResult = auth(req, ModelProvider.XAI);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const response = await request(req);
return response;
} catch (e) {
console.error("[XAI] ", e);
return NextResponse.json(prettyObject(e));
}
}
async function request(req: NextRequest) {
const controller = new AbortController();
// alibaba use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, "");
let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
},
method: req.method,
body: req.body,
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
// #1815 try to refuse some request to some models
if (serverConfig.customModels && req.body) {
try {
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody) as { model?: string };
// not undefined and is false
if (
isModelAvailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.XAI as string,
)
) {
return NextResponse.json(
{
error: true,
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,
},
);
}
} catch (e) {
console.error(`[XAI] filter`, e);
}
}
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}

View File

@ -1,22 +1,32 @@
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { import {
ACCESS_CODE_PREFIX, ACCESS_CODE_PREFIX,
Azure,
ModelProvider, ModelProvider,
ServiceProvider, ServiceProvider,
} from "../constant"; } from "../constant";
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; import {
import { ChatGPTApi } from "./platforms/openai"; ChatMessageTool,
ChatMessage,
ModelType,
useAccessStore,
useChatStore,
} from "../store";
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
import { GeminiProApi } from "./platforms/google"; import { GeminiProApi } from "./platforms/google";
import { ClaudeApi } from "./platforms/anthropic"; import { ClaudeApi } from "./platforms/anthropic";
import { ErnieApi } from "./platforms/baidu"; import { ErnieApi } from "./platforms/baidu";
import { DoubaoApi } from "./platforms/bytedance"; import { DoubaoApi } from "./platforms/bytedance";
import { QwenApi } from "./platforms/alibaba"; import { QwenApi } from "./platforms/alibaba";
import { HunyuanApi } from "./platforms/tencent";
import { MoonshotApi } from "./platforms/moonshot";
import { SparkApi } from "./platforms/iflytek";
import { XAIApi } from "./platforms/xai";
export const ROLES = ["system", "user", "assistant"] as const; export const ROLES = ["system", "user", "assistant"] as const;
export type MessageRole = (typeof ROLES)[number]; export type MessageRole = (typeof ROLES)[number];
export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
export const TTSModels = ["tts-1", "tts-1-hd"] as const;
export type ChatModel = ModelType; export type ChatModel = ModelType;
export interface MultimodalContent { export interface MultimodalContent {
@ -40,6 +50,18 @@ export interface LLMConfig {
stream?: boolean; stream?: boolean;
presence_penalty?: number; presence_penalty?: number;
frequency_penalty?: number; frequency_penalty?: number;
size?: DalleRequestPayload["size"];
quality?: DalleRequestPayload["quality"];
style?: DalleRequestPayload["style"];
}
export interface SpeechOptions {
model: string;
input: string;
voice: string;
response_format?: string;
speed?: number;
onController?: (controller: AbortController) => void;
} }
export interface ChatOptions { export interface ChatOptions {
@ -50,6 +72,8 @@ export interface ChatOptions {
onFinish: (message: string) => void; onFinish: (message: string) => void;
onError?: (err: Error) => void; onError?: (err: Error) => void;
onController?: (controller: AbortController) => void; onController?: (controller: AbortController) => void;
onBeforeTool?: (tool: ChatMessageTool) => void;
onAfterTool?: (tool: ChatMessageTool) => void;
} }
export interface LLMUsage { export interface LLMUsage {
@ -62,16 +86,19 @@ export interface LLMModel {
displayName?: string; displayName?: string;
available: boolean; available: boolean;
provider: LLMModelProvider; provider: LLMModelProvider;
sorted: number;
} }
export interface LLMModelProvider { export interface LLMModelProvider {
id: string; id: string;
providerName: string; providerName: string;
providerType: string; providerType: string;
sorted: number;
} }
export abstract class LLMApi { export abstract class LLMApi {
abstract chat(options: ChatOptions): Promise<void>; abstract chat(options: ChatOptions): Promise<void>;
abstract speech(options: SpeechOptions): Promise<ArrayBuffer>;
abstract usage(): Promise<LLMUsage>; abstract usage(): Promise<LLMUsage>;
abstract models(): Promise<LLMModel[]>; abstract models(): Promise<LLMModel[]>;
} }
@ -117,6 +144,18 @@ export class ClientApi {
case ModelProvider.Qwen: case ModelProvider.Qwen:
this.llm = new QwenApi(); this.llm = new QwenApi();
break; break;
case ModelProvider.Hunyuan:
this.llm = new HunyuanApi();
break;
case ModelProvider.Moonshot:
this.llm = new MoonshotApi();
break;
case ModelProvider.Iflytek:
this.llm = new SparkApi();
break;
case ModelProvider.XAI:
this.llm = new XAIApi();
break;
default: default:
this.llm = new ChatGPTApi(); this.llm = new ChatGPTApi();
} }
@ -168,24 +207,43 @@ export class ClientApi {
} }
} }
export function getHeaders() { export function getBearerToken(
apiKey: string,
noBearer: boolean = false,
): string {
return validString(apiKey)
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
: "";
}
export function validString(x: string): boolean {
return x?.length > 0;
}
export function getHeaders(ignoreHeaders: boolean = false) {
const accessStore = useAccessStore.getState(); const accessStore = useAccessStore.getState();
const chatStore = useChatStore.getState(); const chatStore = useChatStore.getState();
const headers: Record<string, string> = { let headers: Record<string, string> = {};
"Content-Type": "application/json", if (!ignoreHeaders) {
Accept: "application/json", headers = {
}; "Content-Type": "application/json",
Accept: "application/json",
};
}
const clientConfig = getClientConfig(); const clientConfig = getClientConfig();
function getConfig() { function getConfig() {
const modelConfig = chatStore.currentSession().mask.modelConfig; const modelConfig = chatStore.currentSession().mask.modelConfig;
const isGoogle = modelConfig.providerName == ServiceProvider.Google; const isGoogle = modelConfig.providerName === ServiceProvider.Google;
const isAzure = modelConfig.providerName === ServiceProvider.Azure; const isAzure = modelConfig.providerName === ServiceProvider.Azure;
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic; const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu; const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance; const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba; const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
const isXAI = modelConfig.providerName === ServiceProvider.XAI;
const isEnabledAccessControl = accessStore.enabledAccessControl(); const isEnabledAccessControl = accessStore.enabledAccessControl();
const apiKey = isGoogle const apiKey = isGoogle
? accessStore.googleApiKey ? accessStore.googleApiKey
@ -197,6 +255,14 @@ export function getHeaders() {
? accessStore.bytedanceApiKey ? accessStore.bytedanceApiKey
: isAlibaba : isAlibaba
? accessStore.alibabaApiKey ? accessStore.alibabaApiKey
: isMoonshot
? accessStore.moonshotApiKey
: isXAI
? accessStore.xaiApiKey
: isIflytek
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
: ""
: accessStore.openaiApiKey; : accessStore.openaiApiKey;
return { return {
isGoogle, isGoogle,
@ -205,24 +271,24 @@ export function getHeaders() {
isBaidu, isBaidu,
isByteDance, isByteDance,
isAlibaba, isAlibaba,
isMoonshot,
isIflytek,
isXAI,
apiKey, apiKey,
isEnabledAccessControl, isEnabledAccessControl,
}; };
} }
function getAuthHeader(): string { function getAuthHeader(): string {
return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization"; return isAzure
? "api-key"
: isAnthropic
? "x-api-key"
: isGoogle
? "x-goog-api-key"
: "Authorization";
} }
function getBearerToken(apiKey: string, noBearer: boolean = false): string {
return validString(apiKey)
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
: "";
}
function validString(x: string): boolean {
return x?.length > 0;
}
const { const {
isGoogle, isGoogle,
isAzure, isAzure,
@ -231,14 +297,15 @@ export function getHeaders() {
apiKey, apiKey,
isEnabledAccessControl, isEnabledAccessControl,
} = getConfig(); } = getConfig();
// when using google api in app, not set auth header
if (isGoogle && clientConfig?.isApp) return headers;
// when using baidu api in app, not set auth header // when using baidu api in app, not set auth header
if (isBaidu && clientConfig?.isApp) return headers; if (isBaidu && clientConfig?.isApp) return headers;
const authHeader = getAuthHeader(); const authHeader = getAuthHeader();
const bearerToken = getBearerToken(apiKey, isAzure || isAnthropic); const bearerToken = getBearerToken(
apiKey,
isAzure || isAnthropic || isGoogle,
);
if (bearerToken) { if (bearerToken) {
headers[authHeader] = bearerToken; headers[authHeader] = bearerToken;
@ -263,6 +330,14 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
return new ClientApi(ModelProvider.Doubao); return new ClientApi(ModelProvider.Doubao);
case ServiceProvider.Alibaba: case ServiceProvider.Alibaba:
return new ClientApi(ModelProvider.Qwen); return new ClientApi(ModelProvider.Qwen);
case ServiceProvider.Tencent:
return new ClientApi(ModelProvider.Hunyuan);
case ServiceProvider.Moonshot:
return new ClientApi(ModelProvider.Moonshot);
case ServiceProvider.Iflytek:
return new ClientApi(ModelProvider.Iflytek);
case ServiceProvider.XAI:
return new ClientApi(ModelProvider.XAI);
default: default:
return new ClientApi(ModelProvider.GPT); return new ClientApi(ModelProvider.GPT);
} }

View File

@ -12,6 +12,7 @@ import {
getHeaders, getHeaders,
LLMApi, LLMApi,
LLMModel, LLMModel,
SpeechOptions,
MultimodalContent, MultimodalContent,
} from "../api"; } from "../api";
import Locale from "../../locales"; import Locale from "../../locales";
@ -21,7 +22,8 @@ import {
} from "@fortaine/fetch-event-source"; } from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client"; import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent, isVisionModel } from "@/app/utils"; import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse { export interface OpenAIListModelResponse {
object: string; object: string;
@ -83,6 +85,10 @@ export class QwenApi implements LLMApi {
return res?.output?.choices?.at(0)?.message?.content ?? ""; return res?.output?.choices?.at(0)?.message?.content ?? "";
} }
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) { async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({ const messages = options.messages.map((v) => ({
role: v.role, role: v.role,
@ -173,6 +179,7 @@ export class QwenApi implements LLMApi {
controller.signal.onabort = finish; controller.signal.onabort = finish;
fetchEventSource(chatPath, { fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload, ...chatPayload,
async onopen(res) { async onopen(res) {
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);

View File

@ -1,18 +1,19 @@
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant"; import { Anthropic, ApiPath } from "@/app/constant";
import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api"; import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { DEFAULT_API_HOST } from "@/app/constant";
import { RequestMessage } from "@/app/typing";
import { import {
EventStreamContentType, useAccessStore,
fetchEventSource, useAppConfig,
} from "@fortaine/fetch-event-source"; useChatStore,
usePluginStore,
import Locale from "../../locales"; ChatMessageTool,
import { prettyObject } from "@/app/utils/format"; } from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { ANTHROPIC_BASE_URL } from "@/app/constant";
import { getMessageTextContent, isVisionModel } from "@/app/utils"; import { getMessageTextContent, isVisionModel } from "@/app/utils";
import { preProcessImageContent, stream } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export type MultiBlockContent = { export type MultiBlockContent = {
type: "image" | "text"; type: "image" | "text";
@ -73,6 +74,10 @@ const ClaudeMapper = {
const keys = ["claude-2, claude-instant-1"]; const keys = ["claude-2, claude-instant-1"];
export class ClaudeApi implements LLMApi { export class ClaudeApi implements LLMApi {
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
extractMessage(res: any) { extractMessage(res: any) {
console.log("[Response] claude response: ", res); console.log("[Response] claude response: ", res);
@ -93,7 +98,12 @@ export class ClaudeApi implements LLMApi {
}, },
}; };
const messages = [...options.messages]; // try get base64image from local cache image_url
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = await preProcessImageContent(v.content);
messages.push({ role: v.role, content });
}
const keys = ["system", "user"]; const keys = ["system", "user"];
@ -186,112 +196,126 @@ export class ClaudeApi implements LLMApi {
const controller = new AbortController(); const controller = new AbortController();
options.onController?.(controller); options.onController?.(controller);
const payload = {
method: "POST",
body: JSON.stringify(requestBody),
signal: controller.signal,
headers: {
...getHeaders(), // get common headers
"anthropic-version": accessStore.anthropicApiVersion,
// do not send `anthropicApiKey` in browser!!!
// Authorization: getAuthKey(accessStore.anthropicApiKey),
},
};
if (shouldStream) { if (shouldStream) {
try { let index = -1;
const context = { const [tools, funcs] = usePluginStore
text: "", .getState()
finished: false, .getAsTools(
}; useChatStore.getState().currentSession().mask?.plugin || [],
);
const finish = () => { return stream(
if (!context.finished) { path,
options.onFinish(context.text); requestBody,
context.finished = true; {
} ...getHeaders(),
}; "anthropic-version": accessStore.anthropicApiVersion,
},
controller.signal.onabort = finish; // @ts-ignore
fetchEventSource(path, { tools.map((tool) => ({
...payload, name: tool?.function?.name,
async onopen(res) { description: tool?.function?.description,
const contentType = res.headers.get("content-type"); input_schema: tool?.function?.parameters,
console.log("response content type: ", contentType); })),
funcs,
if (contentType?.startsWith("text/plain")) { controller,
context.text = await res.clone().text(); // parseSSE
return finish(); (text: string, runTools: ChatMessageTool[]) => {
} // console.log("parseSSE", text, runTools);
let chunkJson:
if ( | undefined
!res.ok || | {
!res.headers type: "content_block_delta" | "content_block_stop";
.get("content-type") content_block?: {
?.startsWith(EventStreamContentType) || type: "tool_use";
res.status !== 200 id: string;
) { name: string;
const responseTexts = [context.text];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
context.text = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
let chunkJson:
| undefined
| {
type: "content_block_delta" | "content_block_stop";
delta?: {
type: "text_delta";
text: string;
};
index: number;
}; };
try { delta?: {
chunkJson = JSON.parse(msg.data); type: "text_delta" | "input_json_delta";
} catch (e) { text?: string;
console.error("[Response] parse error", msg.data); partial_json?: string;
} };
index: number;
};
chunkJson = JSON.parse(text);
if (!chunkJson || chunkJson.type === "content_block_stop") { if (chunkJson?.content_block?.type == "tool_use") {
return finish(); index += 1;
} const id = chunkJson?.content_block.id;
const name = chunkJson?.content_block.name;
const { delta } = chunkJson; runTools.push({
if (delta?.text) { id,
context.text += delta.text; type: "function",
options.onUpdate?.(context.text, delta.text); function: {
} name,
}, arguments: "",
onclose() { },
finish(); });
}, }
onerror(e) { if (
options.onError?.(e); chunkJson?.delta?.type == "input_json_delta" &&
throw e; chunkJson?.delta?.partial_json
}, ) {
openWhenHidden: true, // @ts-ignore
}); runTools[index]["function"]["arguments"] +=
} catch (e) { chunkJson?.delta?.partial_json;
console.error("failed to chat", e); }
options.onError?.(e as Error); return chunkJson?.delta?.text;
} },
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// reset index value
index = -1;
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
{
role: "assistant",
content: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
type: "tool_use",
id: tool.id,
name: tool?.function?.name,
input: tool?.function?.arguments
? JSON.parse(tool?.function?.arguments)
: {},
}),
),
},
// @ts-ignore
...toolCallResult.map((result) => ({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: result.tool_call_id,
content: result.content,
},
],
})),
);
},
options,
);
} else { } else {
const payload = {
method: "POST",
body: JSON.stringify(requestBody),
signal: controller.signal,
headers: {
...getHeaders(), // get common headers
"anthropic-version": accessStore.anthropicApiVersion,
// do not send `anthropicApiKey` in browser!!!
// Authorization: getAuthKey(accessStore.anthropicApiKey),
},
};
try { try {
controller.signal.onabort = () => options.onFinish(""); controller.signal.onabort = () => options.onFinish("");
@ -365,9 +389,7 @@ export class ClaudeApi implements LLMApi {
if (baseUrl.trim().length === 0) { if (baseUrl.trim().length === 0) {
const isApp = !!getClientConfig()?.isApp; const isApp = !!getClientConfig()?.isApp;
baseUrl = isApp baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
? DEFAULT_API_HOST + "/api/proxy/anthropic"
: ApiPath.Anthropic;
} }
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) { if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {

View File

@ -14,6 +14,7 @@ import {
LLMApi, LLMApi,
LLMModel, LLMModel,
MultimodalContent, MultimodalContent,
SpeechOptions,
} from "../api"; } from "../api";
import Locale from "../../locales"; import Locale from "../../locales";
import { import {
@ -23,6 +24,7 @@ import {
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client"; import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils"; import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse { export interface OpenAIListModelResponse {
object: string; object: string;
@ -75,18 +77,30 @@ export class ErnieApi implements LLMApi {
return [baseUrl, path].join("/"); return [baseUrl, path].join("/");
} }
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) { async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({ const messages = options.messages.map((v) => ({
role: v.role, // "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
role: v.role === "system" ? "user" : v.role,
content: getMessageTextContent(v), content: getMessageTextContent(v),
})); }));
// "error_code": 336006, "error_msg": "the length of messages must be an odd number", // "error_code": 336006, "error_msg": "the length of messages must be an odd number",
if (messages.length % 2 === 0) { if (messages.length % 2 === 0) {
messages.unshift({ if (messages.at(0)?.role === "user") {
role: "user", messages.splice(1, 0, {
content: " ", role: "assistant",
}); content: " ",
});
} else {
messages.unshift({
role: "user",
content: " ",
});
}
} }
const modelConfig = { const modelConfig = {
@ -184,6 +198,7 @@ export class ErnieApi implements LLMApi {
controller.signal.onabort = finish; controller.signal.onabort = finish;
fetchEventSource(chatPath, { fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload, ...chatPayload,
async onopen(res) { async onopen(res) {
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);

View File

@ -13,6 +13,7 @@ import {
LLMApi, LLMApi,
LLMModel, LLMModel,
MultimodalContent, MultimodalContent,
SpeechOptions,
} from "../api"; } from "../api";
import Locale from "../../locales"; import Locale from "../../locales";
import { import {
@ -22,6 +23,7 @@ import {
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client"; import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils"; import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse { export interface OpenAIListModelResponse {
object: string; object: string;
@ -77,6 +79,10 @@ export class DoubaoApi implements LLMApi {
return res.choices?.at(0)?.message?.content ?? ""; return res.choices?.at(0)?.message?.content ?? "";
} }
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) { async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({ const messages = options.messages.map((v) => ({
role: v.role, role: v.role,
@ -160,6 +166,7 @@ export class DoubaoApi implements LLMApi {
controller.signal.onabort = finish; controller.signal.onabort = finish;
fetchEventSource(chatPath, { fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload, ...chatPayload,
async onopen(res) { async onopen(res) {
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);

View File

@ -1,21 +1,60 @@
import { Google, REQUEST_TIMEOUT_MS } from "@/app/constant"; import { ApiPath, Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { DEFAULT_API_HOST } from "@/app/constant";
import Locale from "../../locales";
import { import {
EventStreamContentType, ChatOptions,
fetchEventSource, getHeaders,
} from "@fortaine/fetch-event-source"; LLMApi,
import { prettyObject } from "@/app/utils/format"; LLMModel,
LLMUsage,
SpeechOptions,
} from "../api";
import {
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
ChatMessageTool,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
import { getClientConfig } from "@/app/config/client";
import { GEMINI_BASE_URL } from "@/app/constant";
import { import {
getMessageTextContent, getMessageTextContent,
getMessageImages, getMessageImages,
isVisionModel, isVisionModel,
} from "@/app/utils"; } from "@/app/utils";
import { preProcessImageContent } from "@/app/utils/chat";
import { nanoid } from "nanoid";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export class GeminiProApi implements LLMApi { export class GeminiProApi implements LLMApi {
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.googleUrl;
}
const isApp = !!getClientConfig()?.isApp;
if (baseUrl.length === 0) {
baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Google)) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
let chatPath = [baseUrl, path].join("/");
chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
return chatPath;
}
extractMessage(res: any) { extractMessage(res: any) {
console.log("[Response] gemini-pro response: ", res); console.log("[Response] gemini-pro response: ", res);
@ -25,10 +64,21 @@ export class GeminiProApi implements LLMApi {
"" ""
); );
} }
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions): Promise<void> { async chat(options: ChatOptions): Promise<void> {
const apiClient = this; const apiClient = this;
let multimodal = false; let multimodal = false;
const messages = options.messages.map((v) => {
// try get base64image from local cache image_url
const _messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = await preProcessImageContent(v.content);
_messages.push({ role: v.role, content });
}
const messages = _messages.map((v) => {
let parts: any[] = [{ text: getMessageTextContent(v) }]; let parts: any[] = [{ text: getMessageTextContent(v) }];
if (isVisionModel(options.config.model)) { if (isVisionModel(options.config.model)) {
const images = getMessageImages(v); const images = getMessageImages(v);
@ -70,6 +120,9 @@ export class GeminiProApi implements LLMApi {
// if (visionModel && messages.length > 1) { // if (visionModel && messages.length > 1) {
// options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision")); // options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
// } // }
const accessStore = useAccessStore.getState();
const modelConfig = { const modelConfig = {
...useAppConfig.getState().modelConfig, ...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig, ...useChatStore.getState().currentSession().mask.modelConfig,
@ -91,47 +144,30 @@ export class GeminiProApi implements LLMApi {
safetySettings: [ safetySettings: [
{ {
category: "HARM_CATEGORY_HARASSMENT", category: "HARM_CATEGORY_HARASSMENT",
threshold: "BLOCK_ONLY_HIGH", threshold: accessStore.googleSafetySettings,
}, },
{ {
category: "HARM_CATEGORY_HATE_SPEECH", category: "HARM_CATEGORY_HATE_SPEECH",
threshold: "BLOCK_ONLY_HIGH", threshold: accessStore.googleSafetySettings,
}, },
{ {
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
threshold: "BLOCK_ONLY_HIGH", threshold: accessStore.googleSafetySettings,
}, },
{ {
category: "HARM_CATEGORY_DANGEROUS_CONTENT", category: "HARM_CATEGORY_DANGEROUS_CONTENT",
threshold: "BLOCK_ONLY_HIGH", threshold: accessStore.googleSafetySettings,
}, },
], ],
}; };
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.googleUrl;
}
const isApp = !!getClientConfig()?.isApp;
let shouldStream = !!options.config.stream; let shouldStream = !!options.config.stream;
const controller = new AbortController(); const controller = new AbortController();
options.onController?.(controller); options.onController?.(controller);
try { try {
if (!baseUrl && isApp) { // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
baseUrl = DEFAULT_API_HOST + "/api/proxy/google/"; const chatPath = this.path(Google.ChatPath(modelConfig.model));
}
baseUrl = `${baseUrl}/${Google.ChatPath(modelConfig.model)}`.replaceAll(
"//",
"/",
);
if (isApp) {
baseUrl += `?key=${accessStore.googleApiKey}`;
}
const chatPayload = { const chatPayload = {
method: "POST", method: "POST",
body: JSON.stringify(requestPayload), body: JSON.stringify(requestPayload),
@ -146,120 +182,86 @@ export class GeminiProApi implements LLMApi {
); );
if (shouldStream) { if (shouldStream) {
let responseText = ""; const [tools, funcs] = usePluginStore
let remainText = ""; .getState()
let finished = false; .getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
chatPath,
requestPayload,
getHeaders(),
// @ts-ignore
tools.length > 0
? // @ts-ignore
[{ functionDeclarations: tools.map((tool) => tool.function) }]
: [],
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const chunkJson = JSON.parse(text);
const finish = () => { const functionCall = chunkJson?.candidates
if (!finished) { ?.at(0)
finished = true; ?.content.parts.at(0)?.functionCall;
options.onFinish(responseText + remainText); if (functionCall) {
} const { name, args } = functionCall;
}; runTools.push({
id: nanoid(),
// animate response to make it looks smooth type: "function",
function animateResponseText() { function: {
if (finished || controller.signal.aborted) { name,
responseText += remainText; arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
finish(); },
return; });
} }
return chunkJson?.candidates?.at(0)?.content.parts.at(0)?.text;
if (remainText.length > 0) { },
const fetchCount = Math.max(1, Math.round(remainText.length / 60)); // processToolMessage, include tool_calls message and tool call results
const fetchText = remainText.slice(0, fetchCount); (
responseText += fetchText; requestPayload: RequestPayload,
remainText = remainText.slice(fetchCount); toolCallMessage: any,
options.onUpdate?.(responseText, fetchText); toolCallResult: any[],
} ) => {
// @ts-ignore
requestAnimationFrame(animateResponseText); requestPayload?.contents?.splice(
} // @ts-ignore
requestPayload?.contents?.length,
// start animaion 0,
animateResponseText(); {
role: "model",
controller.signal.onabort = finish; parts: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
// https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb functionCall: {
const chatPath = name: tool?.function?.name,
baseUrl.replace("generateContent", "streamGenerateContent") + args: JSON.parse(tool?.function?.arguments as string),
(baseUrl.indexOf("?") > -1 ? "&alt=sse" : "?alt=sse"); },
fetchEventSource(chatPath, { }),
...chatPayload, ),
async onopen(res) { },
clearTimeout(requestTimeoutId); // @ts-ignore
const contentType = res.headers.get("content-type"); ...toolCallResult.map((result) => ({
console.log( role: "function",
"[Gemini] request response content type: ", parts: [
contentType, {
functionResponse: {
name: result.name,
response: {
name: result.name,
content: result.content, // TODO just text content...
},
},
},
],
})),
); );
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
return finish();
}
}, },
onmessage(msg) { options,
if (msg.data === "[DONE]" || finished) { );
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const delta = apiClient.extractMessage(json);
if (delta) {
remainText += delta;
}
const blockReason = json?.promptFeedback?.blockReason;
if (blockReason) {
// being blocked
console.log(`[Google] [Safety Ratings] result:`, blockReason);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else { } else {
const res = await fetch(baseUrl, chatPayload); const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
const resJson = await res.json(); const resJson = await res.json();
if (resJson?.promptFeedback?.blockReason) { if (resJson?.promptFeedback?.blockReason) {
@ -285,14 +287,4 @@ export class GeminiProApi implements LLMApi {
async models(): Promise<LLMModel[]> { async models(): Promise<LLMModel[]> {
return []; return [];
} }
path(path: string): string {
return "/api/google/" + path;
}
}
function ensureProperEnding(str: string) {
if (str.startsWith("[") && !str.endsWith("]")) {
return str + "]";
}
return str;
} }

View File

@ -0,0 +1,252 @@
"use client";
import {
ApiPath,
IFLYTEK_BASE_URL,
Iflytek,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
import { RequestPayload } from "./openai";
export class SparkApi implements LLMApi {
private disableListModels = true;
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.iflytekUrl;
}
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath.Iflytek;
baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
return [baseUrl, path].join("/");
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
messages.push({ role: v.role, content });
}
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
...{
model: options.config.model,
providerName: options.config.providerName,
},
};
const requestPayload: RequestPayload = {
messages,
stream: options.config.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p,
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
};
console.log("[Request] Spark payload: ", requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
const chatPath = this.path(Iflytek.ChatPath);
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// Make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let finished = false;
// Animate response text to make it look smooth
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
return;
}
if (remainText.length > 0) {
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
const fetchText = remainText.slice(0, fetchCount);
responseText += fetchText;
remainText = remainText.slice(fetchCount);
options.onUpdate?.(responseText, fetchText);
}
requestAnimationFrame(animateResponseText);
}
// Start animation
animateResponseText();
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
}
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log("[Spark] request response content type: ", contentType);
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
// Handle different error scenarios
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
extraInfo = Locale.Error.Unauthorized;
}
options.onError?.(
new Error(
`Request failed with status ${res.status}: ${extraInfo}`,
),
);
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: { content: string };
}>;
const delta = choices[0]?.delta?.content;
if (delta) {
remainText += delta;
}
} catch (e) {
console.error("[Request] parse error", text);
options.onError?.(new Error(`Failed to parse response: ${text}`));
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
if (!res.ok) {
const errorText = await res.text();
options.onError?.(
new Error(`Request failed with status ${res.status}: ${errorText}`),
);
return;
}
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,
total: 0,
};
}
async models(): Promise<LLMModel[]> {
return [];
}
}

View File

@ -0,0 +1,200 @@
"use client";
// azure and openai, using same models. so using same LLMApi.
import {
ApiPath,
MOONSHOT_BASE_URL,
Moonshot,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export class MoonshotApi implements LLMApi {
private disableListModels = true;
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.moonshotUrl;
}
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath.Moonshot;
baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Moonshot)) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
return [baseUrl, path].join("/");
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
messages.push({ role: v.role, content });
}
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
...{
model: options.config.model,
providerName: options.config.providerName,
},
};
const requestPayload: RequestPayload = {
messages,
stream: options.config.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p,
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
};
console.log("[Request] openai payload: ", requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
const chatPath = this.path(Moonshot.ChatPath);
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (shouldStream) {
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
chatPath,
requestPayload,
getHeaders(),
tools as any,
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
content: string;
tool_calls: ChatMessageTool[];
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
const index = tool_calls[0]?.index;
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name as string,
arguments: args,
},
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
}
}
return choices[0]?.delta?.content;
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
toolCallMessage,
...toolCallResult,
);
},
options,
);
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,
total: 0,
};
}
async models(): Promise<LLMModel[]> {
return [];
}
}

View File

@ -2,16 +2,29 @@
// azure and openai, using same models. so using same LLMApi. // azure and openai, using same models. so using same LLMApi.
import { import {
ApiPath, ApiPath,
DEFAULT_API_HOST, OPENAI_BASE_URL,
DEFAULT_MODELS, DEFAULT_MODELS,
OpenaiPath, OpenaiPath,
Azure, Azure,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import {
ChatMessageTool,
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
} from "@/app/store";
import { collectModelsWithDefaultModel } from "@/app/utils/model"; import { collectModelsWithDefaultModel } from "@/app/utils/model";
import {
preProcessImageContent,
uploadImage,
base64Image2Blob,
stream,
} from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
import { import {
ChatOptions, ChatOptions,
@ -20,19 +33,16 @@ import {
LLMModel, LLMModel,
LLMUsage, LLMUsage,
MultimodalContent, MultimodalContent,
SpeechOptions,
} from "../api"; } from "../api";
import Locale from "../../locales"; import Locale from "../../locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client"; import { getClientConfig } from "@/app/config/client";
import { import {
getMessageTextContent, getMessageTextContent,
getMessageImages,
isVisionModel, isVisionModel,
isDalle3 as _isDalle3,
} from "@/app/utils"; } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse { export interface OpenAIListModelResponse {
object: string; object: string;
@ -57,6 +67,16 @@ export interface RequestPayload {
max_tokens?: number; max_tokens?: number;
} }
export interface DalleRequestPayload {
model: string;
prompt: string;
response_format: "url" | "b64_json";
n: number;
size: DalleSize;
quality: DalleQuality;
style: DalleStyle;
}
export class ChatGPTApi implements LLMApi { export class ChatGPTApi implements LLMApi {
private disableListModels = true; private disableListModels = true;
@ -79,7 +99,7 @@ export class ChatGPTApi implements LLMApi {
if (baseUrl.length === 0) { if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp; const isApp = !!getClientConfig()?.isApp;
const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI; const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath; baseUrl = isApp ? OPENAI_BASE_URL : apiPath;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith("/")) {
@ -99,17 +119,69 @@ export class ChatGPTApi implements LLMApi {
return cloudflareAIGatewayUrl([baseUrl, path].join("/")); return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
} }
extractMessage(res: any) { async extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? ""; if (res.error) {
return "```\n" + JSON.stringify(res, null, 4) + "\n```";
}
// dalle3 model return url, using url create image message
if (res.data) {
let url = res.data?.at(0)?.url ?? "";
const b64_json = res.data?.at(0)?.b64_json ?? "";
if (!url && b64_json) {
// uploadImage
url = await uploadImage(base64Image2Blob(b64_json, "image/png"));
}
return [
{
type: "image_url",
image_url: {
url,
},
},
];
}
return res.choices?.at(0)?.message?.content ?? res;
}
async speech(options: SpeechOptions): Promise<ArrayBuffer> {
const requestPayload = {
model: options.model,
input: options.input,
voice: options.voice,
response_format: options.response_format,
speed: options.speed,
};
console.log("[Request] openai speech payload: ", requestPayload);
const controller = new AbortController();
options.onController?.(controller);
try {
const speechPath = this.path(OpenaiPath.SpeechPath);
const speechPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
const res = await fetch(speechPath, speechPayload);
clearTimeout(requestTimeoutId);
return await res.arrayBuffer();
} catch (e) {
console.log("[Request] failed to make a speech request", e);
throw e;
}
} }
async chat(options: ChatOptions) { async chat(options: ChatOptions) {
const visionModel = isVisionModel(options.config.model);
const messages = options.messages.map((v) => ({
role: v.role,
content: visionModel ? v.content : getMessageTextContent(v),
}));
const modelConfig = { const modelConfig = {
...useAppConfig.getState().modelConfig, ...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig, ...useChatStore.getState().currentSession().mask.modelConfig,
@ -119,26 +191,57 @@ export class ChatGPTApi implements LLMApi {
}, },
}; };
const requestPayload: RequestPayload = { let requestPayload: RequestPayload | DalleRequestPayload;
messages,
stream: options.config.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p,
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
};
// add max_tokens to vision model const isDalle3 = _isDalle3(options.config.model);
if (visionModel && modelConfig.model.includes("preview")) { const isO1 = options.config.model.startsWith("o1");
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); if (isDalle3) {
const prompt = getMessageTextContent(
options.messages.slice(-1)?.pop() as any,
);
requestPayload = {
model: options.config.model,
prompt,
// URLs are only valid for 60 minutes after the image has been generated.
response_format: "b64_json", // using b64_json, and save image in CacheStorage
n: 1,
size: options.config?.size ?? "1024x1024",
quality: options.config?.quality ?? "standard",
style: options.config?.style ?? "vivid",
};
} else {
const visionModel = isVisionModel(options.config.model);
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = visionModel
? await preProcessImageContent(v.content)
: getMessageTextContent(v);
if (!(isO1 && v.role === "system"))
messages.push({ role: v.role, content });
}
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
requestPayload = {
messages,
stream: !isO1 ? options.config.stream : false,
model: modelConfig.model,
temperature: !isO1 ? modelConfig.temperature : 1,
presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0,
top_p: !isO1 ? modelConfig.top_p : 1,
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
};
// add max_tokens to vision model
if (visionModel) {
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
}
} }
console.log("[Request] openai payload: ", requestPayload); console.log("[Request] openai payload: ", requestPayload);
const shouldStream = !!options.config.stream; const shouldStream = !isDalle3 && !!options.config.stream && !isO1;
const controller = new AbortController(); const controller = new AbortController();
options.onController?.(controller); options.onController?.(controller);
@ -164,156 +267,100 @@ export class ChatGPTApi implements LLMApi {
model?.provider?.providerName === ServiceProvider.Azure, model?.provider?.providerName === ServiceProvider.Azure,
); );
chatPath = this.path( chatPath = this.path(
Azure.ChatPath( (isDalle3 ? Azure.ImagePath : Azure.ChatPath)(
(model?.displayName ?? model?.name) as string, (model?.displayName ?? model?.name) as string,
useCustomConfig ? useAccessStore.getState().azureApiVersion : "", useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
), ),
); );
} else { } else {
chatPath = this.path(OpenaiPath.ChatPath); chatPath = this.path(
isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
);
} }
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (shouldStream) { if (shouldStream) {
let responseText = ""; let index = -1;
let remainText = ""; const [tools, funcs] = usePluginStore
let finished = false; .getState()
.getAsTools(
// animate response to make it looks smooth useChatStore.getState().currentSession().mask?.plugin || [],
function animateResponseText() { );
if (finished || controller.signal.aborted) { // console.log("getAsTools", tools, funcs);
responseText += remainText; stream(
console.log("[Response Animation] finished"); chatPath,
if (responseText?.length === 0) { requestPayload,
options.onError?.(new Error("empty response from server")); getHeaders(),
tools as any,
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
content: string;
tool_calls: ChatMessageTool[];
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
index += 1;
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name as string,
arguments: args,
},
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
}
} }
return; return choices[0]?.delta?.content;
} },
// processToolMessage, include tool_calls message and tool call results
if (remainText.length > 0) { (
const fetchCount = Math.max(1, Math.round(remainText.length / 60)); requestPayload: RequestPayload,
const fetchText = remainText.slice(0, fetchCount); toolCallMessage: any,
responseText += fetchText; toolCallResult: any[],
remainText = remainText.slice(fetchCount); ) => {
options.onUpdate?.(responseText, fetchText); // reset index value
} index = -1;
// @ts-ignore
requestAnimationFrame(animateResponseText); requestPayload?.messages?.splice(
} // @ts-ignore
requestPayload?.messages?.length,
// start animaion 0,
animateResponseText(); toolCallMessage,
...toolCallResult,
const finish = () => { );
if (!finished) { },
finished = true; options,
options.onFinish(responseText + remainText); );
} } else {
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
}; };
controller.signal.onabort = finish; // make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
);
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[OpenAI] request response content type: ",
contentType,
);
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: { content: string };
}>;
const delta = choices[0]?.delta?.content;
const textmoderation = json?.prompt_filter_results;
if (delta) {
remainText += delta;
}
if (
textmoderation &&
textmoderation.length > 0 &&
ServiceProvider.Azure
) {
const contentFilterResults =
textmoderation[0]?.content_filter_results;
console.log(
`[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
contentFilterResults,
);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else {
const res = await fetch(chatPath, chatPayload); const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
const resJson = await res.json(); const resJson = await res.json();
const message = this.extractMessage(resJson); const message = await this.extractMessage(resJson);
options.onFinish(message); options.onFinish(message);
} }
} catch (e) { } catch (e) {
@ -400,20 +447,26 @@ export class ChatGPTApi implements LLMApi {
}); });
const resJson = (await res.json()) as OpenAIListModelResponse; const resJson = (await res.json()) as OpenAIListModelResponse;
const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-")); const chatModels = resJson.data?.filter(
(m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
);
console.log("[Models]", chatModels); console.log("[Models]", chatModels);
if (!chatModels) { if (!chatModels) {
return []; return [];
} }
//由于目前 OpenAI 的 disableListModels 默认为 true所以当前实际不会运行到这场
let seq = 1000; //同 Constant.ts 中的排序保持一致
return chatModels.map((m) => ({ return chatModels.map((m) => ({
name: m.id, name: m.id,
available: true, available: true,
sorted: seq++,
provider: { provider: {
id: "openai", id: "openai",
providerName: "OpenAI", providerName: "OpenAI",
providerType: "openai", providerType: "openai",
sorted: 1,
}, },
})); }));
} }

View File

@ -0,0 +1,273 @@
"use client";
import { ApiPath, TENCENT_BASE_URL, REQUEST_TIMEOUT_MS } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent, isVisionModel } from "@/app/utils";
import mapKeys from "lodash-es/mapKeys";
import mapValues from "lodash-es/mapValues";
import isArray from "lodash-es/isArray";
import isObject from "lodash-es/isObject";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
object: string;
data: Array<{
id: string;
object: string;
root: string;
}>;
}
interface RequestPayload {
Messages: {
Role: "system" | "user" | "assistant";
Content: string | MultimodalContent[];
}[];
Stream?: boolean;
Model: string;
Temperature: number;
TopP: number;
}
function capitalizeKeys(obj: any): any {
if (isArray(obj)) {
return obj.map(capitalizeKeys);
} else if (isObject(obj)) {
return mapValues(
mapKeys(obj, (value: any, key: string) =>
key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase()),
),
capitalizeKeys,
);
} else {
return obj;
}
}
export class HunyuanApi implements LLMApi {
path(): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.tencentUrl;
}
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Tencent)) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl);
return baseUrl;
}
extractMessage(res: any) {
return res.Choices?.at(0)?.Message?.Content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const visionModel = isVisionModel(options.config.model);
const messages = options.messages.map((v, index) => ({
// "Messages 中 system 角色必须位于列表的最开始"
role: index !== 0 && v.role === "system" ? "user" : v.role,
content: visionModel ? v.content : getMessageTextContent(v),
}));
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
...{
model: options.config.model,
},
};
const requestPayload: RequestPayload = capitalizeKeys({
model: modelConfig.model,
messages,
temperature: modelConfig.temperature,
top_p: modelConfig.top_p,
stream: options.config.stream,
});
console.log("[Request] Tencent payload: ", requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
const chatPath = this.path();
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let finished = false;
// animate response to make it looks smooth
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
if (responseText?.length === 0) {
options.onError?.(new Error("empty response from server"));
}
return;
}
if (remainText.length > 0) {
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
const fetchText = remainText.slice(0, fetchCount);
responseText += fetchText;
remainText = remainText.slice(fetchCount);
options.onUpdate?.(responseText, fetchText);
}
requestAnimationFrame(animateResponseText);
}
// start animaion
animateResponseText();
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
}
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[Tencent] request response content type: ",
contentType,
);
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const choices = json.Choices as Array<{
Delta: { Content: string };
}>;
const delta = choices[0]?.Delta?.Content;
if (delta) {
remainText += delta;
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,
total: 0,
};
}
async models(): Promise<LLMModel[]> {
return [];
}
}

193
app/client/platforms/xai.ts Normal file
View File

@ -0,0 +1,193 @@
"use client";
// azure and openai, using same models. so using same LLMApi.
import { ApiPath, XAI_BASE_URL, XAI, REQUEST_TIMEOUT_MS } from "@/app/constant";
import {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export class XAIApi implements LLMApi {
private disableListModels = true;
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.xaiUrl;
}
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath.XAI;
baseUrl = isApp ? XAI_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
return [baseUrl, path].join("/");
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
messages.push({ role: v.role, content });
}
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
...{
model: options.config.model,
providerName: options.config.providerName,
},
};
const requestPayload: RequestPayload = {
messages,
stream: options.config.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p,
};
console.log("[Request] xai payload: ", requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
const chatPath = this.path(XAI.ChatPath);
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (shouldStream) {
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
chatPath,
requestPayload,
getHeaders(),
tools as any,
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
content: string;
tool_calls: ChatMessageTool[];
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
const index = tool_calls[0]?.index;
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name as string,
arguments: args,
},
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
}
}
return choices[0]?.delta?.content;
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
toolCallMessage,
...toolCallResult,
);
},
options,
);
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,
total: 0,
};
}
async models(): Promise<LLMModel[]> {
return [];
}
}

View File

@ -38,16 +38,20 @@ interface ChatCommands {
next?: Command; next?: Command;
prev?: Command; prev?: Command;
clear?: Command; clear?: Command;
fork?: Command;
del?: Command; del?: Command;
} }
export const ChatCommandPrefix = ":"; // Compatible with Chinese colon character ""
export const ChatCommandPrefix = /^[:]/;
export function useChatCommand(commands: ChatCommands = {}) { export function useChatCommand(commands: ChatCommands = {}) {
function extract(userInput: string) { function extract(userInput: string) {
return ( const match = userInput.match(ChatCommandPrefix);
userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput if (match) {
) as keyof ChatCommands; return userInput.slice(1) as keyof ChatCommands;
}
return userInput as keyof ChatCommands;
} }
function search(userInput: string) { function search(userInput: string) {
@ -57,7 +61,7 @@ export function useChatCommand(commands: ChatCommands = {}) {
.filter((c) => c.startsWith(input)) .filter((c) => c.startsWith(input))
.map((c) => ({ .map((c) => ({
title: desc[c as keyof ChatCommands], title: desc[c as keyof ChatCommands],
content: ChatCommandPrefix + c, content: ":" + c,
})); }));
} }

View File

@ -0,0 +1,31 @@
.artifacts {
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
&-header {
display: flex;
align-items: center;
height: 36px;
padding: 20px;
background: var(--second);
}
&-title {
flex: 1;
text-align: center;
font-weight: bold;
font-size: 24px;
}
&-content {
flex-grow: 1;
padding: 0 20px 20px 20px;
background-color: var(--second);
}
}
.artifacts-iframe {
width: 100%;
border: var(--border-in-light);
border-radius: 6px;
background-color: var(--gray);
}

View File

@ -0,0 +1,266 @@
import {
useEffect,
useState,
useRef,
useMemo,
forwardRef,
useImperativeHandle,
} from "react";
import { useParams } from "react-router";
import { IconButton } from "./button";
import { nanoid } from "nanoid";
import ExportIcon from "../icons/share.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
import GithubIcon from "../icons/github.svg";
import LoadingButtonIcon from "../icons/loading.svg";
import ReloadButtonIcon from "../icons/reload.svg";
import Locale from "../locales";
import { Modal, showToast } from "./ui-lib";
import { copyToClipboard, downloadAs } from "../utils";
import { Path, ApiPath, REPO_URL } from "@/app/constant";
import { Loading } from "./home";
import styles from "./artifacts.module.scss";
type HTMLPreviewProps = {
code: string;
autoHeight?: boolean;
height?: number | string;
onLoad?: (title?: string) => void;
};
export type HTMLPreviewHander = {
reload: () => void;
};
export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
function HTMLPreview(props, ref) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [frameId, setFrameId] = useState<string>(nanoid());
const [iframeHeight, setIframeHeight] = useState(600);
const [title, setTitle] = useState("");
/*
* https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
* 1. using srcdoc
* 2. using src with dataurl:
* easy to share
* length limit (Data URIs cannot be larger than 32,768 characters.)
*/
useEffect(() => {
const handleMessage = (e: any) => {
const { id, height, title } = e.data;
setTitle(title);
if (id == frameId) {
setIframeHeight(height);
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [frameId]);
useImperativeHandle(ref, () => ({
reload: () => {
setFrameId(nanoid());
},
}));
const height = useMemo(() => {
if (!props.autoHeight) return props.height || 600;
if (typeof props.height === "string") {
return props.height;
}
const parentHeight = props.height || 600;
return iframeHeight + 40 > parentHeight
? parentHeight
: iframeHeight + 40;
}, [props.autoHeight, props.height, iframeHeight]);
const srcDoc = useMemo(() => {
const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
if (props.code.includes("<!DOCTYPE html>")) {
props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
}
return script + props.code;
}, [props.code, frameId]);
const handleOnLoad = () => {
if (props?.onLoad) {
props.onLoad(title);
}
};
return (
<iframe
className={styles["artifacts-iframe"]}
key={frameId}
ref={iframeRef}
sandbox="allow-forms allow-modals allow-scripts"
style={{ height }}
srcDoc={srcDoc}
onLoad={handleOnLoad}
/>
);
},
);
export function ArtifactsShareButton({
getCode,
id,
style,
fileName,
}: {
getCode: () => string;
id?: string;
style?: any;
fileName?: string;
}) {
const [loading, setLoading] = useState(false);
const [name, setName] = useState(id);
const [show, setShow] = useState(false);
const shareUrl = useMemo(
() => [location.origin, "#", Path.Artifacts, "/", name].join(""),
[name],
);
const upload = (code: string) =>
id
? Promise.resolve({ id })
: fetch(ApiPath.Artifacts, {
method: "POST",
body: code,
})
.then((res) => res.json())
.then(({ id }) => {
if (id) {
return { id };
}
throw Error();
})
.catch((e) => {
showToast(Locale.Export.Artifacts.Error);
});
return (
<>
<div className="window-action-button" style={style}>
<IconButton
icon={loading ? <LoadingButtonIcon /> : <ExportIcon />}
bordered
title={Locale.Export.Artifacts.Title}
onClick={() => {
if (loading) return;
setLoading(true);
upload(getCode())
.then((res) => {
if (res?.id) {
setShow(true);
setName(res?.id);
}
})
.finally(() => setLoading(false));
}}
/>
</div>
{show && (
<div className="modal-mask">
<Modal
title={Locale.Export.Artifacts.Title}
onClose={() => setShow(false)}
actions={[
<IconButton
key="download"
icon={<DownloadIcon />}
bordered
text={Locale.Export.Download}
onClick={() => {
downloadAs(getCode(), `${fileName || name}.html`).then(() =>
setShow(false),
);
}}
/>,
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.Chat.Actions.Copy}
onClick={() => {
copyToClipboard(shareUrl).then(() => setShow(false));
}}
/>,
]}
>
<div>
<a target="_blank" href={shareUrl}>
{shareUrl}
</a>
</div>
</Modal>
</div>
)}
</>
);
}
export function Artifacts() {
const { id } = useParams();
const [code, setCode] = useState("");
const [loading, setLoading] = useState(true);
const [fileName, setFileName] = useState("");
const previewRef = useRef<HTMLPreviewHander>(null);
useEffect(() => {
if (id) {
fetch(`${ApiPath.Artifacts}?id=${id}`)
.then((res) => {
if (res.status > 300) {
throw Error("can not get content");
}
return res;
})
.then((res) => res.text())
.then(setCode)
.catch((e) => {
showToast(Locale.Export.Artifacts.Error);
});
}
}, [id]);
return (
<div className={styles["artifacts"]}>
<div className={styles["artifacts-header"]}>
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
<IconButton bordered icon={<GithubIcon />} shadow />
</a>
<IconButton
bordered
style={{ marginLeft: 20 }}
icon={<ReloadButtonIcon />}
shadow
onClick={() => previewRef.current?.reload()}
/>
<div className={styles["artifacts-title"]}>NextChat Artifacts</div>
<ArtifactsShareButton
id={id}
getCode={() => code}
fileName={fileName}
/>
</div>
<div className={styles["artifacts-content"]}>
{loading && <Loading />}
{code && (
<HTMLPreview
code={code}
ref={previewRef}
autoHeight={false}
height={"100%"}
onLoad={(title) => {
setFileName(title as string);
setLoading(false);
}}
/>
)}
</div>
</div>
);
}

View File

@ -1,12 +1,70 @@
.auth-page { .auth-page {
display: flex; display: flex;
justify-content: center; justify-content: flex-start;
align-items: center; align-items: center;
height: 100%; height: 100%;
width: 100%; width: 100%;
flex-direction: column; flex-direction: column;
.top-banner {
position: relative;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 12px 64px;
box-sizing: border-box;
background: var(--second);
.top-banner-inner {
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
line-height: 150%;
span {
gap: 8px;
a {
display: inline-flex;
align-items: center;
text-decoration: none;
margin-left: 8px;
color: var(--primary);
}
}
}
.top-banner-close {
cursor: pointer;
position: absolute;
top: 50%;
right: 48px;
transform: translateY(-50%);
}
}
@media (max-width: 600px) {
.top-banner {
padding: 12px 24px 12px 12px;
.top-banner-close {
right: 10px;
}
.top-banner-inner {
.top-banner-logo {
margin-right: 8px;
}
}
}
}
.auth-header {
display: flex;
justify-content: space-between;
width: 100%;
padding: 10px;
box-sizing: border-box;
animation: slide-in-from-top ease 0.3s;
}
.auth-logo { .auth-logo {
margin-top: 10vh;
transform: scale(1.4); transform: scale(1.4);
} }
@ -14,6 +72,7 @@
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
line-height: 2; line-height: 2;
margin-bottom: 1vh;
} }
.auth-tips { .auth-tips {
@ -24,6 +83,10 @@
margin: 3vh 0; margin: 3vh 0;
} }
.auth-input-second {
margin: 0 0 3vh 0;
}
.auth-actions { .auth-actions {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -1,21 +1,35 @@
import styles from "./auth.module.scss"; import styles from "./auth.module.scss";
import { IconButton } from "./button"; import { IconButton } from "./button";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Path } from "../constant"; import { Path, SAAS_CHAT_URL } from "../constant";
import { useAccessStore } from "../store"; import { useAccessStore } from "../store";
import Locale from "../locales"; import Locale from "../locales";
import Delete from "../icons/close.svg";
import Arrow from "../icons/arrow.svg";
import Logo from "../icons/logo.svg";
import { useMobileScreen } from "@/app/utils";
import BotIcon from "../icons/bot.svg"; import BotIcon from "../icons/bot.svg";
import { useEffect } from "react";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { PasswordInput } from "./ui-lib";
import LeftIcon from "@/app/icons/left.svg";
import { safeLocalStorage } from "@/app/utils";
import {
trackSettingsPageGuideToCPaymentClick,
trackAuthorizationPageButtonToCPaymentClick,
} from "../utils/auth-settings-events";
const storage = safeLocalStorage();
export function AuthPage() { export function AuthPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const goHome = () => navigate(Path.Home); const goHome = () => navigate(Path.Home);
const goChat = () => navigate(Path.Chat); const goChat = () => navigate(Path.Chat);
const goSaas = () => {
trackAuthorizationPageButtonToCPaymentClick();
window.location.href = SAAS_CHAT_URL;
};
const resetAccessCode = () => { const resetAccessCode = () => {
accessStore.update((access) => { accessStore.update((access) => {
access.openaiApiKey = ""; access.openaiApiKey = "";
@ -32,6 +46,14 @@ export function AuthPage() {
return ( return (
<div className={styles["auth-page"]}> <div className={styles["auth-page"]}>
<TopBanner></TopBanner>
<div className={styles["auth-header"]}>
<IconButton
icon={<LeftIcon />}
text={Locale.Auth.Return}
onClick={() => navigate(Path.Home)}
></IconButton>
</div>
<div className={`no-dark ${styles["auth-logo"]}`}> <div className={`no-dark ${styles["auth-logo"]}`}>
<BotIcon /> <BotIcon />
</div> </div>
@ -39,36 +61,43 @@ export function AuthPage() {
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div> <div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div> <div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
<input <PasswordInput
className={styles["auth-input"]} style={{ marginTop: "3vh", marginBottom: "3vh" }}
type="password" aria={Locale.Settings.ShowPassword}
placeholder={Locale.Auth.Input} aria-label={Locale.Auth.Input}
value={accessStore.accessCode} value={accessStore.accessCode}
type="text"
placeholder={Locale.Auth.Input}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.accessCode = e.currentTarget.value), (access) => (access.accessCode = e.currentTarget.value),
); );
}} }}
/> />
{!accessStore.hideUserApiKey ? ( {!accessStore.hideUserApiKey ? (
<> <>
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div> <div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
<input <PasswordInput
className={styles["auth-input"]} style={{ marginTop: "3vh", marginBottom: "3vh" }}
type="password" aria={Locale.Settings.ShowPassword}
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder} aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
value={accessStore.openaiApiKey} value={accessStore.openaiApiKey}
type="text"
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.openaiApiKey = e.currentTarget.value), (access) => (access.openaiApiKey = e.currentTarget.value),
); );
}} }}
/> />
<input <PasswordInput
className={styles["auth-input"]} style={{ marginTop: "3vh", marginBottom: "3vh" }}
type="password" aria={Locale.Settings.ShowPassword}
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder} aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
value={accessStore.googleApiKey} value={accessStore.googleApiKey}
type="text"
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.googleApiKey = e.currentTarget.value), (access) => (access.googleApiKey = e.currentTarget.value),
@ -85,13 +114,74 @@ export function AuthPage() {
onClick={goChat} onClick={goChat}
/> />
<IconButton <IconButton
text={Locale.Auth.Later} text={Locale.Auth.SaasTips}
onClick={() => { onClick={() => {
resetAccessCode(); goSaas();
goHome();
}} }}
/> />
</div> </div>
</div> </div>
); );
} }
function TopBanner() {
const [isHovered, setIsHovered] = useState(false);
const [isVisible, setIsVisible] = useState(true);
const isMobile = useMobileScreen();
useEffect(() => {
// 检查 localStorage 中是否有标记
const bannerDismissed = storage.getItem("bannerDismissed");
// 如果标记不存在,存储默认值并显示横幅
if (!bannerDismissed) {
storage.setItem("bannerDismissed", "false");
setIsVisible(true); // 显示横幅
} else if (bannerDismissed === "true") {
// 如果标记为 "true",则隐藏横幅
setIsVisible(false);
}
}, []);
const handleMouseEnter = () => {
setIsHovered(true);
};
const handleMouseLeave = () => {
setIsHovered(false);
};
const handleClose = () => {
setIsVisible(false);
storage.setItem("bannerDismissed", "true");
};
if (!isVisible) {
return null;
}
return (
<div
className={styles["top-banner"]}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className={`${styles["top-banner-inner"]} no-dark`}>
<Logo className={styles["top-banner-logo"]}></Logo>
<span>
{Locale.Auth.TopTips}
<a
href={SAAS_CHAT_URL}
rel="stylesheet"
onClick={() => {
trackSettingsPageGuideToCPaymentClick();
}}
>
{Locale.Settings.Access.SaasStart.ChatNow}
<Arrow style={{ marginLeft: "4px" }} />
</a>
</span>
</div>
{(isHovered || isMobile) && (
<Delete className={styles["top-banner-close"]} onClick={handleClose} />
)}
</div>
);
}

View File

@ -5,7 +5,6 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 10px; padding: 10px;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
overflow: hidden; overflow: hidden;

View File

@ -1,6 +1,7 @@
import * as React from "react"; import * as React from "react";
import styles from "./button.module.scss"; import styles from "./button.module.scss";
import { CSSProperties } from "react";
export type ButtonType = "primary" | "danger" | null; export type ButtonType = "primary" | "danger" | null;
@ -16,6 +17,8 @@ export function IconButton(props: {
disabled?: boolean; disabled?: boolean;
tabIndex?: number; tabIndex?: number;
autoFocus?: boolean; autoFocus?: boolean;
style?: CSSProperties;
aria?: string;
}) { }) {
return ( return (
<button <button
@ -31,9 +34,12 @@ export function IconButton(props: {
role="button" role="button"
tabIndex={props.tabIndex} tabIndex={props.tabIndex}
autoFocus={props.autoFocus} autoFocus={props.autoFocus}
style={props.style}
aria-label={props.aria}
> >
{props.icon && ( {props.icon && (
<div <div
aria-label={props.text || props.title}
className={ className={
styles["icon-button-icon"] + styles["icon-button-icon"] +
` ${props.type === "primary" && "no-dark"}` ` ${props.type === "primary" && "no-dark"}`
@ -44,7 +50,12 @@ export function IconButton(props: {
)} )}
{props.text && ( {props.text && (
<div className={styles["icon-button-text"]}>{props.text}</div> <div
aria-label={props.text || props.title}
className={styles["icon-button-text"]}
>
{props.text}
</div>
)} )}
</button> </button>
); );

View File

@ -1,5 +1,4 @@
import DeleteIcon from "../icons/delete.svg"; import DeleteIcon from "../icons/delete.svg";
import BotIcon from "../icons/bot.svg";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
import { import {
@ -12,7 +11,7 @@ import {
import { useChatStore } from "../store"; import { useChatStore } from "../store";
import Locale from "../locales"; import Locale from "../locales";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { Path } from "../constant"; import { Path } from "../constant";
import { MaskAvatar } from "./mask"; import { MaskAvatar } from "./mask";
import { Mask } from "../store/mask"; import { Mask } from "../store/mask";

View File

@ -346,6 +346,12 @@
flex-wrap: nowrap; flex-wrap: nowrap;
} }
} }
.chat-model-name {
font-size: 12px;
color: var(--black);
margin-left: 6px;
}
} }
.chat-message-container { .chat-message-container {
@ -407,6 +413,21 @@
margin-top: 5px; margin-top: 5px;
} }
.chat-message-tools {
font-size: 12px;
color: #aaa;
line-height: 1.5;
margin-top: 5px;
.chat-message-tool {
display: flex;
align-items: end;
svg {
margin-left: 5px;
margin-right: 5px;
}
}
}
.chat-message-item { .chat-message-item {
box-sizing: border-box; box-sizing: border-box;
max-width: 100%; max-width: 100%;
@ -625,3 +646,51 @@
bottom: 30px; bottom: 30px;
} }
} }
.shortcut-key-container {
padding: 10px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.shortcut-key-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 16px;
}
.shortcut-key-item {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
padding: 10px;
background-color: var(--white);
}
.shortcut-key-title {
font-size: 14px;
color: var(--black);
}
.shortcut-key-keys {
display: flex;
gap: 8px;
}
.shortcut-key {
display: flex;
align-items: center;
justify-content: center;
border: var(--border-in-light);
border-radius: 8px;
padding: 4px;
background-color: var(--gray);
min-width: 32px;
}
.shortcut-key span {
font-size: 12px;
color: var(--black);
}

View File

@ -15,6 +15,8 @@ import RenameIcon from "../icons/rename.svg";
import ExportIcon from "../icons/share.svg"; import ExportIcon from "../icons/share.svg";
import ReturnIcon from "../icons/return.svg"; import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg"; import CopyIcon from "../icons/copy.svg";
import SpeakIcon from "../icons/speak.svg";
import SpeakStopIcon from "../icons/speak-stop.svg";
import LoadingIcon from "../icons/three-dots.svg"; import LoadingIcon from "../icons/three-dots.svg";
import LoadingButtonIcon from "../icons/loading.svg"; import LoadingButtonIcon from "../icons/loading.svg";
import PromptIcon from "../icons/prompt.svg"; import PromptIcon from "../icons/prompt.svg";
@ -28,6 +30,7 @@ import DeleteIcon from "../icons/clear.svg";
import PinIcon from "../icons/pin.svg"; import PinIcon from "../icons/pin.svg";
import EditIcon from "../icons/rename.svg"; import EditIcon from "../icons/rename.svg";
import ConfirmIcon from "../icons/confirm.svg"; import ConfirmIcon from "../icons/confirm.svg";
import CloseIcon from "../icons/close.svg";
import CancelIcon from "../icons/cancel.svg"; import CancelIcon from "../icons/cancel.svg";
import ImageIcon from "../icons/image.svg"; import ImageIcon from "../icons/image.svg";
@ -37,6 +40,12 @@ import AutoIcon from "../icons/auto.svg";
import BottomIcon from "../icons/bottom.svg"; import BottomIcon from "../icons/bottom.svg";
import StopIcon from "../icons/pause.svg"; import StopIcon from "../icons/pause.svg";
import RobotIcon from "../icons/robot.svg"; import RobotIcon from "../icons/robot.svg";
import SizeIcon from "../icons/size.svg";
import QualityIcon from "../icons/hd.svg";
import StyleIcon from "../icons/palette.svg";
import PluginIcon from "../icons/plugin.svg";
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
import ReloadIcon from "../icons/reload.svg";
import { import {
ChatMessage, ChatMessage,
@ -49,6 +58,7 @@ import {
useAppConfig, useAppConfig,
DEFAULT_TOPIC, DEFAULT_TOPIC,
ModelType, ModelType,
usePluginStore,
} from "../store"; } from "../store";
import { import {
@ -59,13 +69,17 @@ import {
getMessageTextContent, getMessageTextContent,
getMessageImages, getMessageImages,
isVisionModel, isVisionModel,
isDalle3,
showPlugins,
safeLocalStorage,
} from "../utils"; } from "../utils";
import { compressImage } from "@/app/utils/chat"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { ChatControllerPool } from "../client/controller"; import { ChatControllerPool } from "../client/controller";
import { DalleSize, DalleQuality, DalleStyle } from "../typing";
import { Prompt, usePromptStore } from "../store/prompt"; import { Prompt, usePromptStore } from "../store/prompt";
import Locale from "../locales"; import Locale from "../locales";
@ -84,7 +98,8 @@ import {
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import {
CHAT_PAGE_SIZE, CHAT_PAGE_SIZE,
LAST_INPUT_KEY, DEFAULT_TTS_ENGINE,
ModelProvider,
Path, Path,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
UNFINISHED_INPUT, UNFINISHED_INPUT,
@ -100,6 +115,16 @@ import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import { MultimodalContent } from "../client/api"; import { MultimodalContent } from "../client/api";
import { ClientApi } from "../client/api";
import { createTTSPlayer } from "../utils/audio";
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
import { isEmpty } from "lodash-es";
const localStorage = safeLocalStorage();
const ttsPlayer = createTTSPlayer();
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />, loading: () => <LoadingIcon />,
}); });
@ -179,7 +204,7 @@ function PromptToast(props: {
return ( return (
<div className={styles["prompt-toast"]} key="prompt-toast"> <div className={styles["prompt-toast"]} key="prompt-toast">
{props.showToast && ( {props.showToast && context.length > 0 && (
<div <div
className={styles["prompt-toast-inner"] + " clickable"} className={styles["prompt-toast-inner"] + " clickable"}
role="button" role="button"
@ -245,11 +270,11 @@ function useSubmitHandler() {
}; };
} }
export type RenderPompt = Pick<Prompt, "title" | "content">; export type RenderPrompt = Pick<Prompt, "title" | "content">;
export function PromptHints(props: { export function PromptHints(props: {
prompts: RenderPompt[]; prompts: RenderPrompt[];
onPromptSelect: (prompt: RenderPompt) => void; onPromptSelect: (prompt: RenderPrompt) => void;
}) { }) {
const noPrompts = props.prompts.length === 0; const noPrompts = props.prompts.length === 0;
const [selectIndex, setSelectIndex] = useState(0); const [selectIndex, setSelectIndex] = useState(0);
@ -338,7 +363,7 @@ function ClearContextDivider() {
); );
} }
function ChatAction(props: { export function ChatAction(props: {
text: string; text: string;
icon: JSX.Element; icon: JSX.Element;
onClick: () => void; onClick: () => void;
@ -428,10 +453,13 @@ export function ChatActions(props: {
showPromptHints: () => void; showPromptHints: () => void;
hitBottom: boolean; hitBottom: boolean;
uploading: boolean; uploading: boolean;
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
setUserInput: (input: string) => void;
}) { }) {
const config = useAppConfig(); const config = useAppConfig();
const navigate = useNavigate(); const navigate = useNavigate();
const chatStore = useChatStore(); const chatStore = useChatStore();
const pluginStore = usePluginStore();
// switch themes // switch themes
const theme = config.theme; const theme = config.theme;
@ -476,8 +504,24 @@ export function ChatActions(props: {
return model?.displayName ?? ""; return model?.displayName ?? "";
}, [models, currentModel, currentProviderName]); }, [models, currentModel, currentProviderName]);
const [showModelSelector, setShowModelSelector] = useState(false); const [showModelSelector, setShowModelSelector] = useState(false);
const [showPluginSelector, setShowPluginSelector] = useState(false);
const [showUploadImage, setShowUploadImage] = useState(false); const [showUploadImage, setShowUploadImage] = useState(false);
const [showSizeSelector, setShowSizeSelector] = useState(false);
const [showQualitySelector, setShowQualitySelector] = useState(false);
const [showStyleSelector, setShowStyleSelector] = useState(false);
const dalle3Sizes: DalleSize[] = ["1024x1024", "1792x1024", "1024x1792"];
const dalle3Qualitys: DalleQuality[] = ["standard", "hd"];
const dalle3Styles: DalleStyle[] = ["vivid", "natural"];
const currentSize =
chatStore.currentSession().mask.modelConfig?.size ?? "1024x1024";
const currentQuality =
chatStore.currentSession().mask.modelConfig?.quality ?? "standard";
const currentStyle =
chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
const isMobileScreen = useMobileScreen();
useEffect(() => { useEffect(() => {
const show = isVisionModel(currentModel); const show = isVisionModel(currentModel);
setShowUploadImage(show); setShowUploadImage(show);
@ -488,8 +532,8 @@ export function ChatActions(props: {
// if current model is not available // if current model is not available
// switch to first available model // switch to first available model
const isUnavaliableModel = !models.some((m) => m.name === currentModel); const isUnavailableModel = !models.some((m) => m.name === currentModel);
if (isUnavaliableModel && models.length > 0) { if (isUnavailableModel && models.length > 0) {
// show next model to default model if exist // show next model to default model if exist
let nextModel = models.find((model) => model.isDefault) || models[0]; let nextModel = models.find((model) => model.isDefault) || models[0];
chatStore.updateCurrentSession((session) => { chatStore.updateCurrentSession((session) => {
@ -593,7 +637,7 @@ export function ChatActions(props: {
items={models.map((m) => ({ items={models.map((m) => ({
title: `${m.displayName}${ title: `${m.displayName}${
m?.provider?.providerName m?.provider?.providerName
? "(" + m?.provider?.providerName + ")" ? " (" + m?.provider?.providerName + ")"
: "" : ""
}`, }`,
value: `${m.name}@${m?.provider?.providerName}`, value: `${m.name}@${m?.provider?.providerName}`,
@ -620,6 +664,125 @@ export function ChatActions(props: {
}} }}
/> />
)} )}
{isDalle3(currentModel) && (
<ChatAction
onClick={() => setShowSizeSelector(true)}
text={currentSize}
icon={<SizeIcon />}
/>
)}
{showSizeSelector && (
<Selector
defaultSelectedValue={currentSize}
items={dalle3Sizes.map((m) => ({
title: m,
value: m,
}))}
onClose={() => setShowSizeSelector(false)}
onSelection={(s) => {
if (s.length === 0) return;
const size = s[0];
chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.size = size;
});
showToast(size);
}}
/>
)}
{isDalle3(currentModel) && (
<ChatAction
onClick={() => setShowQualitySelector(true)}
text={currentQuality}
icon={<QualityIcon />}
/>
)}
{showQualitySelector && (
<Selector
defaultSelectedValue={currentQuality}
items={dalle3Qualitys.map((m) => ({
title: m,
value: m,
}))}
onClose={() => setShowQualitySelector(false)}
onSelection={(q) => {
if (q.length === 0) return;
const quality = q[0];
chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.quality = quality;
});
showToast(quality);
}}
/>
)}
{isDalle3(currentModel) && (
<ChatAction
onClick={() => setShowStyleSelector(true)}
text={currentStyle}
icon={<StyleIcon />}
/>
)}
{showStyleSelector && (
<Selector
defaultSelectedValue={currentStyle}
items={dalle3Styles.map((m) => ({
title: m,
value: m,
}))}
onClose={() => setShowStyleSelector(false)}
onSelection={(s) => {
if (s.length === 0) return;
const style = s[0];
chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.style = style;
});
showToast(style);
}}
/>
)}
{showPlugins(currentProviderName, currentModel) && (
<ChatAction
onClick={() => {
if (pluginStore.getAll().length == 0) {
navigate(Path.Plugins);
} else {
setShowPluginSelector(true);
}
}}
text={Locale.Plugin.Name}
icon={<PluginIcon />}
/>
)}
{showPluginSelector && (
<Selector
multiple
defaultSelectedValue={chatStore.currentSession().mask?.plugin}
items={pluginStore.getAll().map((item) => ({
title: `${item?.title}@${item?.version}`,
value: item?.id,
}))}
onClose={() => setShowPluginSelector(false)}
onSelection={(s) => {
chatStore.updateCurrentSession((session) => {
session.mask.plugin = s as string[];
});
}}
/>
)}
{!isMobileScreen && (
<ChatAction
onClick={() => props.setShowShortcutKeyModal(true)}
text={Locale.Chat.ShortcutKey.Title}
icon={<ShortcutkeyIcon />}
/>
)}
</div> </div>
); );
} }
@ -694,6 +857,67 @@ export function DeleteImageButton(props: { deleteImage: () => void }) {
); );
} }
export function ShortcutKeyModal(props: { onClose: () => void }) {
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
const shortcuts = [
{
title: Locale.Chat.ShortcutKey.newChat,
keys: isMac ? ["⌘", "Shift", "O"] : ["Ctrl", "Shift", "O"],
},
{ title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] },
{
title: Locale.Chat.ShortcutKey.copyLastCode,
keys: isMac ? ["⌘", "Shift", ";"] : ["Ctrl", "Shift", ";"],
},
{
title: Locale.Chat.ShortcutKey.copyLastMessage,
keys: isMac ? ["⌘", "Shift", "C"] : ["Ctrl", "Shift", "C"],
},
{
title: Locale.Chat.ShortcutKey.showShortcutKey,
keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"],
},
];
return (
<div className="modal-mask">
<Modal
title={Locale.Chat.ShortcutKey.Title}
onClose={props.onClose}
actions={[
<IconButton
type="primary"
text={Locale.UI.Confirm}
icon={<ConfirmIcon />}
key="ok"
onClick={() => {
props.onClose();
}}
/>,
]}
>
<div className={styles["shortcut-key-container"]}>
<div className={styles["shortcut-key-grid"]}>
{shortcuts.map((shortcut, index) => (
<div key={index} className={styles["shortcut-key-item"]}>
<div className={styles["shortcut-key-title"]}>
{shortcut.title}
</div>
<div className={styles["shortcut-key-keys"]}>
{shortcut.keys.map((key, i) => (
<div key={i} className={styles["shortcut-key"]}>
<span>{key}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
</Modal>
</div>
);
}
function _Chat() { function _Chat() {
type RenderMessage = ChatMessage & { preview?: boolean }; type RenderMessage = ChatMessage & { preview?: boolean };
@ -701,6 +925,7 @@ function _Chat() {
const session = chatStore.currentSession(); const session = chatStore.currentSession();
const config = useAppConfig(); const config = useAppConfig();
const fontSize = config.fontSize; const fontSize = config.fontSize;
const fontFamily = config.fontFamily;
const [showExport, setShowExport] = useState(false); const [showExport, setShowExport] = useState(false);
@ -727,7 +952,7 @@ function _Chat() {
// prompt hints // prompt hints
const promptStore = usePromptStore(); const promptStore = usePromptStore();
const [promptHints, setPromptHints] = useState<RenderPompt[]>([]); const [promptHints, setPromptHints] = useState<RenderPrompt[]>([]);
const onSearch = useDebouncedCallback( const onSearch = useDebouncedCallback(
(text: string) => { (text: string) => {
const matchedPrompts = promptStore.search(text); const matchedPrompts = promptStore.search(text);
@ -768,6 +993,7 @@ function _Chat() {
chatStore.updateCurrentSession( chatStore.updateCurrentSession(
(session) => (session.clearContextIndex = session.messages.length), (session) => (session.clearContextIndex = session.messages.length),
), ),
fork: () => chatStore.forkSession(),
del: () => chatStore.deleteSession(chatStore.currentSessionIndex), del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
}); });
@ -780,7 +1006,7 @@ function _Chat() {
// clear search results // clear search results
if (n === 0) { if (n === 0) {
setPromptHints([]); setPromptHints([]);
} else if (text.startsWith(ChatCommandPrefix)) { } else if (text.match(ChatCommandPrefix)) {
setPromptHints(chatCommands.search(text)); setPromptHints(chatCommands.search(text));
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) { } else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
// check if need to trigger auto completion // check if need to trigger auto completion
@ -792,7 +1018,7 @@ function _Chat() {
}; };
const doSubmit = (userInput: string) => { const doSubmit = (userInput: string) => {
if (userInput.trim() === "") return; if (userInput.trim() === "" && isEmpty(attachImages)) return;
const matchCommand = chatCommands.match(userInput); const matchCommand = chatCommands.match(userInput);
if (matchCommand.matched) { if (matchCommand.matched) {
setUserInput(""); setUserInput("");
@ -805,14 +1031,14 @@ function _Chat() {
.onUserInput(userInput, attachImages) .onUserInput(userInput, attachImages)
.then(() => setIsLoading(false)); .then(() => setIsLoading(false));
setAttachImages([]); setAttachImages([]);
localStorage.setItem(LAST_INPUT_KEY, userInput); chatStore.setLastInput(userInput);
setUserInput(""); setUserInput("");
setPromptHints([]); setPromptHints([]);
if (!isMobileScreen) inputRef.current?.focus(); if (!isMobileScreen) inputRef.current?.focus();
setAutoScroll(true); setAutoScroll(true);
}; };
const onPromptSelect = (prompt: RenderPompt) => { const onPromptSelect = (prompt: RenderPrompt) => {
setTimeout(() => { setTimeout(() => {
setPromptHints([]); setPromptHints([]);
@ -871,7 +1097,7 @@ function _Chat() {
userInput.length <= 0 && userInput.length <= 0 &&
!(e.metaKey || e.altKey || e.ctrlKey) !(e.metaKey || e.altKey || e.ctrlKey)
) { ) {
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? ""); setUserInput(chatStore.lastInput ?? "");
e.preventDefault(); e.preventDefault();
return; return;
} }
@ -971,10 +1197,55 @@ function _Chat() {
}); });
}; };
const accessStore = useAccessStore();
const [speechStatus, setSpeechStatus] = useState(false);
const [speechLoading, setSpeechLoading] = useState(false);
async function openaiSpeech(text: string) {
if (speechStatus) {
ttsPlayer.stop();
setSpeechStatus(false);
} else {
var api: ClientApi;
api = new ClientApi(ModelProvider.GPT);
const config = useAppConfig.getState();
setSpeechLoading(true);
ttsPlayer.init();
let audioBuffer: ArrayBuffer;
const { markdownToTxt } = require("markdown-to-txt");
const textContent = markdownToTxt(text);
if (config.ttsConfig.engine !== DEFAULT_TTS_ENGINE) {
const edgeVoiceName = accessStore.edgeVoiceName();
const tts = new MsEdgeTTS();
await tts.setMetadata(
edgeVoiceName,
OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3,
);
audioBuffer = await tts.toArrayBuffer(textContent);
} else {
audioBuffer = await api.llm.speech({
model: config.ttsConfig.model,
input: textContent,
voice: config.ttsConfig.voice,
speed: config.ttsConfig.speed,
});
}
setSpeechStatus(true);
ttsPlayer
.play(audioBuffer, () => {
setSpeechStatus(false);
})
.catch((e) => {
console.error("[OpenAI Speech]", e);
showToast(prettyObject(e));
setSpeechStatus(false);
})
.finally(() => setSpeechLoading(false));
}
}
const context: RenderMessage[] = useMemo(() => { const context: RenderMessage[] = useMemo(() => {
return session.mask.hideContext ? [] : session.mask.context.slice(); return session.mask.hideContext ? [] : session.mask.context.slice();
}, [session.mask.context, session.mask.hideContext]); }, [session.mask.context, session.mask.hideContext]);
const accessStore = useAccessStore();
if ( if (
context.length === 0 && context.length === 0 &&
@ -1167,7 +1438,7 @@ function _Chat() {
...(await new Promise<string[]>((res, rej) => { ...(await new Promise<string[]>((res, rej) => {
setUploading(true); setUploading(true);
const imagesData: string[] = []; const imagesData: string[] = [];
compressImage(file, 256 * 1024) uploadImageRemote(file)
.then((dataUrl) => { .then((dataUrl) => {
imagesData.push(dataUrl); imagesData.push(dataUrl);
setUploading(false); setUploading(false);
@ -1209,7 +1480,7 @@ function _Chat() {
const imagesData: string[] = []; const imagesData: string[] = [];
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = event.target.files[i]; const file = event.target.files[i];
compressImage(file, 256 * 1024) uploadImageRemote(file)
.then((dataUrl) => { .then((dataUrl) => {
imagesData.push(dataUrl); imagesData.push(dataUrl);
if ( if (
@ -1237,6 +1508,70 @@ function _Chat() {
setAttachImages(images); setAttachImages(images);
} }
// 快捷键 shortcut keys
const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false);
useEffect(() => {
const handleKeyDown = (event: any) => {
// 打开新聊天 command + shift + o
if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.key.toLowerCase() === "o"
) {
event.preventDefault();
setTimeout(() => {
chatStore.newSession();
navigate(Path.Chat);
}, 10);
}
// 聚焦聊天输入 shift + esc
else if (event.shiftKey && event.key.toLowerCase() === "escape") {
event.preventDefault();
inputRef.current?.focus();
}
// 复制最后一个代码块 command + shift + ;
else if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.code === "Semicolon"
) {
event.preventDefault();
const copyCodeButton =
document.querySelectorAll<HTMLElement>(".copy-code-button");
if (copyCodeButton.length > 0) {
copyCodeButton[copyCodeButton.length - 1].click();
}
}
// 复制最后一个回复 command + shift + c
else if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.key.toLowerCase() === "c"
) {
event.preventDefault();
const lastNonUserMessage = messages
.filter((message) => message.role !== "user")
.pop();
if (lastNonUserMessage) {
const lastMessageContent = getMessageTextContent(lastNonUserMessage);
copyToClipboard(lastMessageContent);
}
}
// 展示快捷键 command + /
else if ((event.metaKey || event.ctrlKey) && event.key === "/") {
event.preventDefault();
setShowShortcutKeyModal(true);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [messages, chatStore, navigate]);
return ( return (
<div className={styles.chat} key={session.id}> <div className={styles.chat} key={session.id}>
<div className="window-header" data-tauri-drag-region> <div className="window-header" data-tauri-drag-region>
@ -1265,11 +1600,24 @@ function _Chat() {
</div> </div>
</div> </div>
<div className="window-actions"> <div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<ReloadIcon />}
bordered
title={Locale.Chat.Actions.RefreshTitle}
onClick={() => {
showToast(Locale.Chat.Actions.RefreshToast);
chatStore.summarizeSession(true);
}}
/>
</div>
{!isMobileScreen && ( {!isMobileScreen && (
<div className="window-action-button"> <div className="window-action-button">
<IconButton <IconButton
icon={<RenameIcon />} icon={<RenameIcon />}
bordered bordered
title={Locale.Chat.EditMessage.Title}
aria={Locale.Chat.EditMessage.Title}
onClick={() => setIsEditingMessage(true)} onClick={() => setIsEditingMessage(true)}
/> />
</div> </div>
@ -1289,6 +1637,8 @@ function _Chat() {
<IconButton <IconButton
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />} icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
bordered bordered
title={Locale.Chat.Actions.FullScreen}
aria={Locale.Chat.Actions.FullScreen}
onClick={() => { onClick={() => {
config.update( config.update(
(config) => (config.tightBorder = !config.tightBorder), (config) => (config.tightBorder = !config.tightBorder),
@ -1340,6 +1690,7 @@ function _Chat() {
<div className={styles["chat-message-edit"]}> <div className={styles["chat-message-edit"]}>
<IconButton <IconButton
icon={<EditIcon />} icon={<EditIcon />}
aria={Locale.Chat.Actions.Edit}
onClick={async () => { onClick={async () => {
const newMessage = await showPrompt( const newMessage = await showPrompt(
Locale.Chat.Actions.Edit, Locale.Chat.Actions.Edit,
@ -1388,6 +1739,11 @@ function _Chat() {
</> </>
)} )}
</div> </div>
{!isUser && (
<div className={styles["chat-model-name"]}>
{message.model}
</div>
)}
{showActions && ( {showActions && (
<div className={styles["chat-message-actions"]}> <div className={styles["chat-message-actions"]}>
@ -1426,31 +1782,73 @@ function _Chat() {
) )
} }
/> />
{config.ttsConfig.enable && (
<ChatAction
text={
speechStatus
? Locale.Chat.Actions.StopSpeech
: Locale.Chat.Actions.Speech
}
icon={
speechStatus ? (
<SpeakStopIcon />
) : (
<SpeakIcon />
)
}
onClick={() =>
openaiSpeech(getMessageTextContent(message))
}
/>
)}
</> </>
)} )}
</div> </div>
</div> </div>
)} )}
</div> </div>
{showTyping && ( {message?.tools?.length == 0 && showTyping && (
<div className={styles["chat-message-status"]}> <div className={styles["chat-message-status"]}>
{Locale.Chat.Typing} {Locale.Chat.Typing}
</div> </div>
)} )}
{/*@ts-ignore*/}
{message?.tools?.length > 0 && (
<div className={styles["chat-message-tools"]}>
{message?.tools?.map((tool) => (
<div
key={tool.id}
title={tool?.errorMsg}
className={styles["chat-message-tool"]}
>
{tool.isError === false ? (
<ConfirmIcon />
) : tool.isError === true ? (
<CloseIcon />
) : (
<LoadingButtonIcon />
)}
<span>{tool?.function?.name}</span>
</div>
))}
</div>
)}
<div className={styles["chat-message-item"]}> <div className={styles["chat-message-item"]}>
<Markdown <Markdown
key={message.streaming ? "loading" : "done"}
content={getMessageTextContent(message)} content={getMessageTextContent(message)}
loading={ loading={
(message.preview || message.streaming) && (message.preview || message.streaming) &&
message.content.length === 0 && message.content.length === 0 &&
!isUser !isUser
} }
onContextMenu={(e) => onRightClick(e, message)} // onContextMenu={(e) => onRightClick(e, message)} // hard to use
onDoubleClickCapture={() => { onDoubleClickCapture={() => {
if (!isMobileScreen) return; if (!isMobileScreen) return;
setUserInput(getMessageTextContent(message)); setUserInput(getMessageTextContent(message));
}} }}
fontSize={fontSize} fontSize={fontSize}
fontFamily={fontFamily}
parentRef={scrollRef} parentRef={scrollRef}
defaultShow={i >= messages.length - 6} defaultShow={i >= messages.length - 6}
/> />
@ -1521,6 +1919,8 @@ function _Chat() {
setUserInput("/"); setUserInput("/");
onSearch(""); onSearch("");
}} }}
setShowShortcutKeyModal={setShowShortcutKeyModal}
setUserInput={setUserInput}
/> />
<label <label
className={`${styles["chat-input-panel-inner"]} ${ className={`${styles["chat-input-panel-inner"]} ${
@ -1545,6 +1945,7 @@ function _Chat() {
autoFocus={autoFocus} autoFocus={autoFocus}
style={{ style={{
fontSize: config.fontSize, fontSize: config.fontSize,
fontFamily: config.fontFamily,
}} }}
/> />
{attachImages.length != 0 && ( {attachImages.length != 0 && (
@ -1591,6 +1992,10 @@ function _Chat() {
}} }}
/> />
)} )}
{showShortcutKeyModal && (
<ShortcutKeyModal onClose={() => setShowShortcutKeyModal(false)} />
)}
</div> </div>
); );
} }

View File

@ -36,7 +36,8 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
if (props.model) { if (props.model) {
return ( return (
<div className="no-dark"> <div className="no-dark">
{props.model?.startsWith("gpt-4") ? ( {props.model?.startsWith("gpt-4") ||
props.model?.startsWith("chatgpt-4o") ? (
<BlackBotIcon className="user-avatar" /> <BlackBotIcon className="user-avatar" />
) : ( ) : (
<BotIcon className="user-avatar" /> <BotIcon className="user-avatar" />

View File

@ -1,3 +1,5 @@
"use client";
import React from "react"; import React from "react";
import { IconButton } from "./button"; import { IconButton } from "./button";
import GithubIcon from "../icons/github.svg"; import GithubIcon from "../icons/github.svg";
@ -6,6 +8,7 @@ import { ISSUE_URL } from "../constant";
import Locale from "../locales"; import Locale from "../locales";
import { showConfirm } from "./ui-lib"; import { showConfirm } from "./ui-lib";
import { useSyncStore } from "../store/sync"; import { useSyncStore } from "../store/sync";
import { useChatStore } from "../store/chat";
interface IErrorBoundaryState { interface IErrorBoundaryState {
hasError: boolean; hasError: boolean;
@ -28,8 +31,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
try { try {
useSyncStore.getState().export(); useSyncStore.getState().export();
} finally { } finally {
localStorage.clear(); useChatStore.getState().clearAllData();
location.reload();
} }
} }

View File

@ -1,5 +1,5 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store"; import { ChatMessage, useAppConfig, useChatStore } from "../store";
import Locale from "../locales"; import Locale from "../locales";
import styles from "./exporter.module.scss"; import styles from "./exporter.module.scss";
import { import {
@ -541,7 +541,7 @@ export function ImagePreviewer(props: {
<div> <div>
<div className={styles["main-title"]}>NextChat</div> <div className={styles["main-title"]}>NextChat</div>
<div className={styles["sub-title"]}> <div className={styles["sub-title"]}>
github.com/Yidadaa/ChatGPT-Next-Web github.com/ChatGPTNextWeb/ChatGPT-Next-Web
</div> </div>
<div className={styles["icons"]}> <div className={styles["icons"]}>
<ExportAvatar avatar={config.avatar} /> <ExportAvatar avatar={config.avatar} />
@ -583,6 +583,7 @@ export function ImagePreviewer(props: {
<Markdown <Markdown
content={getMessageTextContent(m)} content={getMessageTextContent(m)}
fontSize={config.fontSize} fontSize={config.fontSize}
fontFamily={config.fontFamily}
defaultShow defaultShow
/> />
{getMessageImages(m).length == 1 && ( {getMessageImages(m).length == 1 && (

View File

@ -137,12 +137,21 @@
position: relative; position: relative;
padding-top: 20px; padding-top: 20px;
padding-bottom: 20px; padding-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
&-narrow {
justify-content: center;
}
} }
.sidebar-logo { .sidebar-logo {
position: absolute; display: inline-flex;
right: 0; }
bottom: 18px;
.sidebar-title-container {
display: inline-flex;
flex-direction: column;
} }
.sidebar-title { .sidebar-title {

View File

@ -39,6 +39,10 @@ export function Loading(props: { noLogo?: boolean }) {
); );
} }
const Artifacts = dynamic(async () => (await import("./artifacts")).Artifacts, {
loading: () => <Loading noLogo />,
});
const Settings = dynamic(async () => (await import("./settings")).Settings, { const Settings = dynamic(async () => (await import("./settings")).Settings, {
loading: () => <Loading noLogo />, loading: () => <Loading noLogo />,
}); });
@ -55,6 +59,21 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
loading: () => <Loading noLogo />, loading: () => <Loading noLogo />,
}); });
const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
loading: () => <Loading noLogo />,
});
const SearchChat = dynamic(
async () => (await import("./search-chat")).SearchChatPage,
{
loading: () => <Loading noLogo />,
},
);
const Sd = dynamic(async () => (await import("./sd")).Sd, {
loading: () => <Loading noLogo />,
});
export function useSwitchTheme() { export function useSwitchTheme() {
const config = useAppConfig(); const config = useAppConfig();
@ -122,11 +141,23 @@ const loadAsyncGoogleFont = () => {
document.head.appendChild(linkEl); document.head.appendChild(linkEl);
}; };
export function WindowContent(props: { children: React.ReactNode }) {
return (
<div className={styles["window-content"]} id={SlotID.AppBody}>
{props?.children}
</div>
);
}
function Screen() { function Screen() {
const config = useAppConfig(); const config = useAppConfig();
const location = useLocation(); const location = useLocation();
const isArtifact = location.pathname.includes(Path.Artifacts);
const isHome = location.pathname === Path.Home; const isHome = location.pathname === Path.Home;
const isAuth = location.pathname === Path.Auth; const isAuth = location.pathname === Path.Auth;
const isSd = location.pathname === Path.Sd;
const isSdNew = location.pathname === Path.SdNew;
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
const shouldTightBorder = const shouldTightBorder =
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen); getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
@ -135,34 +166,42 @@ function Screen() {
loadAsyncGoogleFont(); loadAsyncGoogleFont();
}, []); }, []);
if (isArtifact) {
return (
<Routes>
<Route path="/artifacts/:id" element={<Artifacts />} />
</Routes>
);
}
const renderContent = () => {
if (isAuth) return <AuthPage />;
if (isSd) return <Sd />;
if (isSdNew) return <Sd />;
return (
<>
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
<WindowContent>
<Routes>
<Route path={Path.Home} element={<Chat />} />
<Route path={Path.NewChat} element={<NewChat />} />
<Route path={Path.Masks} element={<MaskPage />} />
<Route path={Path.Plugins} element={<PluginPage />} />
<Route path={Path.SearchChat} element={<SearchChat />} />
<Route path={Path.Chat} element={<Chat />} />
<Route path={Path.Settings} element={<Settings />} />
</Routes>
</WindowContent>
</>
);
};
return ( return (
<div <div
className={ className={`${styles.container} ${
styles.container + shouldTightBorder ? styles["tight-container"] : styles.container
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${ } ${getLang() === "ar" ? styles["rtl-screen"] : ""}`}
getLang() === "ar" ? styles["rtl-screen"] : ""
}`
}
> >
{isAuth ? ( {renderContent()}
<>
<AuthPage />
</>
) : (
<>
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
<div className={styles["window-content"]} id={SlotID.AppBody}>
<Routes>
<Route path={Path.Home} element={<Chat />} />
<Route path={Path.NewChat} element={<NewChat />} />
<Route path={Path.Masks} element={<MaskPage />} />
<Route path={Path.Chat} element={<Chat />} />
<Route path={Path.Settings} element={<Settings />} />
</Routes>
</div>
</>
)}
</div> </div>
); );
} }

View File

@ -9,6 +9,7 @@ interface InputRangeProps {
min: string; min: string;
max: string; max: string;
step: string; step: string;
aria: string;
} }
export function InputRange({ export function InputRange({
@ -19,11 +20,13 @@ export function InputRange({
min, min,
max, max,
step, step,
aria,
}: InputRangeProps) { }: InputRangeProps) {
return ( return (
<div className={styles["input-range"] + ` ${className ?? ""}`}> <div className={styles["input-range"] + ` ${className ?? ""}`}>
{title || value} {title || value}
<input <input
aria-label={aria}
type="range" type="range"
title={title} title={title}
value={value} value={value}

View File

@ -6,13 +6,23 @@ import RehypeKatex from "rehype-katex";
import RemarkGfm from "remark-gfm"; import RemarkGfm from "remark-gfm";
import RehypeHighlight from "rehype-highlight"; import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect, useMemo } from "react"; import { useRef, useState, RefObject, useEffect, useMemo } from "react";
import { copyToClipboard } from "../utils"; import { copyToClipboard, useWindowSize } from "../utils";
import mermaid from "mermaid"; import mermaid from "mermaid";
import Locale from "../locales";
import LoadingIcon from "../icons/three-dots.svg"; import LoadingIcon from "../icons/three-dots.svg";
import ReloadButtonIcon from "../icons/reload.svg";
import React from "react"; import React from "react";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { showImageModal } from "./ui-lib"; import { showImageModal, FullScreen } from "./ui-lib";
import {
ArtifactsShareButton,
HTMLPreview,
HTMLPreviewHander,
} from "./artifacts";
import { useChatStore } from "../store";
import { IconButton } from "./button";
import { useAppConfig } from "../store/config";
export function Mermaid(props: { code: string }) { export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -62,58 +72,150 @@ export function Mermaid(props: { code: string }) {
export function PreCode(props: { children: any }) { export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null); const ref = useRef<HTMLPreElement>(null);
const refText = ref.current?.innerText; const previewRef = useRef<HTMLPreviewHander>(null);
const [mermaidCode, setMermaidCode] = useState(""); const [mermaidCode, setMermaidCode] = useState("");
const [htmlCode, setHtmlCode] = useState("");
const { height } = useWindowSize();
const chatStore = useChatStore();
const session = chatStore.currentSession();
const renderMermaid = useDebouncedCallback(() => { const renderArtifacts = useDebouncedCallback(() => {
if (!ref.current) return; if (!ref.current) return;
const mermaidDom = ref.current.querySelector("code.language-mermaid"); const mermaidDom = ref.current.querySelector("code.language-mermaid");
if (mermaidDom) { if (mermaidDom) {
setMermaidCode((mermaidDom as HTMLElement).innerText); setMermaidCode((mermaidDom as HTMLElement).innerText);
} }
const htmlDom = ref.current.querySelector("code.language-html");
const refText = ref.current.querySelector("code")?.innerText;
if (htmlDom) {
setHtmlCode((htmlDom as HTMLElement).innerText);
} else if (refText?.startsWith("<!DOCTYPE")) {
setHtmlCode(refText);
}
}, 600); }, 600);
const config = useAppConfig();
const enableArtifacts =
session.mask?.enableArtifacts !== false && config.enableArtifacts;
//Wrap the paragraph for plain-text
useEffect(() => { useEffect(() => {
setTimeout(renderMermaid, 1); if (ref.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps const codeElements = ref.current.querySelectorAll(
}, [refText]); "code",
) as NodeListOf<HTMLElement>;
const wrapLanguages = [
"",
"md",
"markdown",
"text",
"txt",
"plaintext",
"tex",
"latex",
];
codeElements.forEach((codeElement) => {
let languageClass = codeElement.className.match(/language-(\w+)/);
let name = languageClass ? languageClass[1] : "";
if (wrapLanguages.includes(name)) {
codeElement.style.whiteSpace = "pre-wrap";
}
});
setTimeout(renderArtifacts, 1);
}
}, []);
return ( return (
<> <>
{mermaidCode.length > 0 && (
<Mermaid code={mermaidCode} key={mermaidCode} />
)}
<pre ref={ref}> <pre ref={ref}>
<span <span
className="copy-code-button" className="copy-code-button"
onClick={() => { onClick={() => {
if (ref.current) { if (ref.current) {
const code = ref.current.innerText; copyToClipboard(
copyToClipboard(code); ref.current.querySelector("code")?.innerText ?? "",
);
} }
}} }}
></span> ></span>
{props.children} {props.children}
</pre> </pre>
{mermaidCode.length > 0 && (
<Mermaid code={mermaidCode} key={mermaidCode} />
)}
{htmlCode.length > 0 && enableArtifacts && (
<FullScreen className="no-dark html" right={70}>
<ArtifactsShareButton
style={{ position: "absolute", right: 20, top: 10 }}
getCode={() => htmlCode}
/>
<IconButton
style={{ position: "absolute", right: 120, top: 10 }}
bordered
icon={<ReloadButtonIcon />}
shadow
onClick={() => previewRef.current?.reload()}
/>
<HTMLPreview
ref={previewRef}
code={htmlCode}
autoHeight={!document.fullscreenElement}
height={!document.fullscreenElement ? 600 : height}
/>
</FullScreen>
)}
</> </>
); );
} }
function escapeDollarNumber(text: string) { function CustomCode(props: { children: any; className?: string }) {
let escapedText = ""; const chatStore = useChatStore();
const session = chatStore.currentSession();
const config = useAppConfig();
const enableCodeFold =
session.mask?.enableCodeFold !== false && config.enableCodeFold;
for (let i = 0; i < text.length; i += 1) { const ref = useRef<HTMLPreElement>(null);
let char = text[i]; const [collapsed, setCollapsed] = useState(true);
const nextChar = text[i + 1] || " "; const [showToggle, setShowToggle] = useState(false);
if (char === "$" && nextChar >= "0" && nextChar <= "9") { useEffect(() => {
char = "\\$"; if (ref.current) {
const codeHeight = ref.current.scrollHeight;
setShowToggle(codeHeight > 400);
ref.current.scrollTop = ref.current.scrollHeight;
} }
}, [props.children]);
escapedText += char; const toggleCollapsed = () => {
} setCollapsed((collapsed) => !collapsed);
};
const renderShowMoreButton = () => {
if (showToggle && enableCodeFold && collapsed) {
return (
<div className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}>
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
</div>
);
}
return null;
};
return (
<>
<code
className={props?.className}
ref={ref}
style={{
maxHeight: enableCodeFold && collapsed ? "400px" : "none",
overflowY: "hidden",
}}
>
{props.children}
</code>
return escapedText; {renderShowMoreButton()}
</>
);
} }
function escapeBrackets(text: string) { function escapeBrackets(text: string) {
@ -134,9 +236,26 @@ function escapeBrackets(text: string) {
); );
} }
function tryWrapHtmlCode(text: string) {
// try add wrap html code (fixed: html codeblock include 2 newline)
return text
.replace(
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
(match, quoteStart, lang, newLine, doctype) => {
return !quoteStart ? "\n```html\n" + doctype : match;
},
)
.replace(
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
},
);
}
function _MarkDownContent(props: { content: string }) { function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(() => { const escapedContent = useMemo(() => {
return escapeBrackets(escapeDollarNumber(props.content)); return tryWrapHtmlCode(escapeBrackets(props.content));
}, [props.content]); }, [props.content]);
return ( return (
@ -154,9 +273,24 @@ function _MarkDownContent(props: { content: string }) {
]} ]}
components={{ components={{
pre: PreCode, pre: PreCode,
code: CustomCode,
p: (pProps) => <p {...pProps} dir="auto" />, p: (pProps) => <p {...pProps} dir="auto" />,
a: (aProps) => { a: (aProps) => {
const href = aProps.href || ""; const href = aProps.href || "";
if (/\.(aac|mp3|opus|wav)$/.test(href)) {
return (
<figure>
<audio controls src={href}></audio>
</figure>
);
}
if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
return (
<video controls width="99.9%">
<source src={href} />
</video>
);
}
const isInternal = /^\/#/i.test(href); const isInternal = /^\/#/i.test(href);
const target = isInternal ? "_self" : aProps.target ?? "_blank"; const target = isInternal ? "_self" : aProps.target ?? "_blank";
return <a {...aProps} target={target} />; return <a {...aProps} target={target} />;
@ -175,6 +309,7 @@ export function Markdown(
content: string; content: string;
loading?: boolean; loading?: boolean;
fontSize?: number; fontSize?: number;
fontFamily?: string;
parentRef?: RefObject<HTMLDivElement>; parentRef?: RefObject<HTMLDivElement>;
defaultShow?: boolean; defaultShow?: boolean;
} & React.DOMAttributes<HTMLDivElement>, } & React.DOMAttributes<HTMLDivElement>,
@ -186,6 +321,7 @@ export function Markdown(
className="markdown-body" className="markdown-body"
style={{ style={{
fontSize: `${props.fontSize ?? 14}px`, fontSize: `${props.fontSize ?? 14}px`,
fontFamily: props.fontFamily || "inherit",
}} }}
ref={mdRef} ref={mdRef}
onContextMenu={props.onContextMenu} onContextMenu={props.onContextMenu}

View File

@ -37,7 +37,7 @@ import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import chatStyle from "./chat.module.scss"; import chatStyle from "./chat.module.scss";
import { useEffect, useState } from "react"; import { useState } from "react";
import { import {
copyToClipboard, copyToClipboard,
downloadAs, downloadAs,
@ -48,7 +48,6 @@ import { Updater } from "../typing";
import { ModelConfigList } from "./model-config"; import { ModelConfigList } from "./model-config";
import { FileName, Path } from "../constant"; import { FileName, Path } from "../constant";
import { BUILTIN_MASK_STORE } from "../masks"; import { BUILTIN_MASK_STORE } from "../masks";
import { nanoid } from "nanoid";
import { import {
DragDropContext, DragDropContext,
Droppable, Droppable,
@ -127,6 +126,8 @@ export function MaskConfig(props: {
onClose={() => setShowPicker(false)} onClose={() => setShowPicker(false)}
> >
<div <div
tabIndex={0}
aria-label={Locale.Mask.Config.Avatar}
onClick={() => setShowPicker(true)} onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
@ -139,6 +140,7 @@ export function MaskConfig(props: {
</ListItem> </ListItem>
<ListItem title={Locale.Mask.Config.Name}> <ListItem title={Locale.Mask.Config.Name}>
<input <input
aria-label={Locale.Mask.Config.Name}
type="text" type="text"
value={props.mask.name} value={props.mask.name}
onInput={(e) => onInput={(e) =>
@ -153,6 +155,7 @@ export function MaskConfig(props: {
subTitle={Locale.Mask.Config.HideContext.SubTitle} subTitle={Locale.Mask.Config.HideContext.SubTitle}
> >
<input <input
aria-label={Locale.Mask.Config.HideContext.Title}
type="checkbox" type="checkbox"
checked={props.mask.hideContext} checked={props.mask.hideContext}
onChange={(e) => { onChange={(e) => {
@ -163,12 +166,48 @@ export function MaskConfig(props: {
></input> ></input>
</ListItem> </ListItem>
{globalConfig.enableArtifacts && (
<ListItem
title={Locale.Mask.Config.Artifacts.Title}
subTitle={Locale.Mask.Config.Artifacts.SubTitle}
>
<input
aria-label={Locale.Mask.Config.Artifacts.Title}
type="checkbox"
checked={props.mask.enableArtifacts !== false}
onChange={(e) => {
props.updateMask((mask) => {
mask.enableArtifacts = e.currentTarget.checked;
});
}}
></input>
</ListItem>
)}
{globalConfig.enableCodeFold && (
<ListItem
title={Locale.Mask.Config.CodeFold.Title}
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
>
<input
aria-label={Locale.Mask.Config.CodeFold.Title}
type="checkbox"
checked={props.mask.enableCodeFold !== false}
onChange={(e) => {
props.updateMask((mask) => {
mask.enableCodeFold = e.currentTarget.checked;
});
}}
></input>
</ListItem>
)}
{!props.shouldSyncFromGlobal ? ( {!props.shouldSyncFromGlobal ? (
<ListItem <ListItem
title={Locale.Mask.Config.Share.Title} title={Locale.Mask.Config.Share.Title}
subTitle={Locale.Mask.Config.Share.SubTitle} subTitle={Locale.Mask.Config.Share.SubTitle}
> >
<IconButton <IconButton
aria={Locale.Mask.Config.Share.Title}
icon={<CopyIcon />} icon={<CopyIcon />}
text={Locale.Mask.Config.Share.Action} text={Locale.Mask.Config.Share.Action}
onClick={copyMaskLink} onClick={copyMaskLink}
@ -182,6 +221,7 @@ export function MaskConfig(props: {
subTitle={Locale.Mask.Config.Sync.SubTitle} subTitle={Locale.Mask.Config.Sync.SubTitle}
> >
<input <input
aria-label={Locale.Mask.Config.Sync.Title}
type="checkbox" type="checkbox"
checked={props.mask.syncGlobalConfig} checked={props.mask.syncGlobalConfig}
onChange={async (e) => { onChange={async (e) => {
@ -404,16 +444,7 @@ export function MaskPage() {
const maskStore = useMaskStore(); const maskStore = useMaskStore();
const chatStore = useChatStore(); const chatStore = useChatStore();
const [filterLang, setFilterLang] = useState<Lang | undefined>( const filterLang = maskStore.language;
() => localStorage.getItem("Mask-language") as Lang | undefined,
);
useEffect(() => {
if (filterLang) {
localStorage.setItem("Mask-language", filterLang);
} else {
localStorage.removeItem("Mask-language");
}
}, [filterLang]);
const allMasks = maskStore const allMasks = maskStore
.getAll() .getAll()
@ -520,9 +551,9 @@ export function MaskPage() {
onChange={(e) => { onChange={(e) => {
const value = e.currentTarget.value; const value = e.currentTarget.value;
if (value === Locale.Settings.Lang.All) { if (value === Locale.Settings.Lang.All) {
setFilterLang(undefined); maskStore.setLanguage(undefined);
} else { } else {
setFilterLang(value as Lang); maskStore.setLanguage(value as Lang);
} }
}} }}
> >

View File

@ -0,0 +1,7 @@
.select-compress-model {
width: 60%;
select {
max-width: 100%;
white-space: normal;
}
}

View File

@ -5,19 +5,28 @@ import Locale from "../locales";
import { InputRange } from "./input-range"; import { InputRange } from "./input-range";
import { ListItem, Select } from "./ui-lib"; import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import { groupBy } from "lodash-es";
import styles from "./model-config.module.scss";
export function ModelConfigList(props: { export function ModelConfigList(props: {
modelConfig: ModelConfig; modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void; updateConfig: (updater: (config: ModelConfig) => void) => void;
}) { }) {
const allModels = useAllModels(); const allModels = useAllModels();
const groupModels = groupBy(
allModels.filter((v) => v.available),
"provider.providerName",
);
const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`; const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
return ( return (
<> <>
<ListItem title={Locale.Settings.Model}> <ListItem title={Locale.Settings.Model}>
<Select <Select
aria-label={Locale.Settings.Model}
value={value} value={value}
align="left"
onChange={(e) => { onChange={(e) => {
const [model, providerName] = e.currentTarget.value.split("@"); const [model, providerName] = e.currentTarget.value.split("@");
props.updateConfig((config) => { props.updateConfig((config) => {
@ -26,13 +35,15 @@ export function ModelConfigList(props: {
}); });
}} }}
> >
{allModels {Object.keys(groupModels).map((providerName, index) => (
.filter((v) => v.available) <optgroup label={providerName} key={index}>
.map((v, i) => ( {groupModels[providerName].map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}> <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName}) {v.displayName}
</option> </option>
))} ))}
</optgroup>
))}
</Select> </Select>
</ListItem> </ListItem>
<ListItem <ListItem
@ -40,6 +51,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.Temperature.SubTitle} subTitle={Locale.Settings.Temperature.SubTitle}
> >
<InputRange <InputRange
aria={Locale.Settings.Temperature.Title}
value={props.modelConfig.temperature?.toFixed(1)} value={props.modelConfig.temperature?.toFixed(1)}
min="0" min="0"
max="1" // lets limit it to 0-1 max="1" // lets limit it to 0-1
@ -59,6 +71,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.TopP.SubTitle} subTitle={Locale.Settings.TopP.SubTitle}
> >
<InputRange <InputRange
aria={Locale.Settings.TopP.Title}
value={(props.modelConfig.top_p ?? 1).toFixed(1)} value={(props.modelConfig.top_p ?? 1).toFixed(1)}
min="0" min="0"
max="1" max="1"
@ -78,6 +91,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.MaxTokens.SubTitle} subTitle={Locale.Settings.MaxTokens.SubTitle}
> >
<input <input
aria-label={Locale.Settings.MaxTokens.Title}
type="number" type="number"
min={1024} min={1024}
max={512000} max={512000}
@ -100,6 +114,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.PresencePenalty.SubTitle} subTitle={Locale.Settings.PresencePenalty.SubTitle}
> >
<InputRange <InputRange
aria={Locale.Settings.PresencePenalty.Title}
value={props.modelConfig.presence_penalty?.toFixed(1)} value={props.modelConfig.presence_penalty?.toFixed(1)}
min="-2" min="-2"
max="2" max="2"
@ -121,6 +136,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.FrequencyPenalty.SubTitle} subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
> >
<InputRange <InputRange
aria={Locale.Settings.FrequencyPenalty.Title}
value={props.modelConfig.frequency_penalty?.toFixed(1)} value={props.modelConfig.frequency_penalty?.toFixed(1)}
min="-2" min="-2"
max="2" max="2"
@ -142,6 +158,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle} subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
> >
<input <input
aria-label={Locale.Settings.InjectSystemPrompts.Title}
type="checkbox" type="checkbox"
checked={props.modelConfig.enableInjectSystemPrompts} checked={props.modelConfig.enableInjectSystemPrompts}
onChange={(e) => onChange={(e) =>
@ -159,6 +176,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.InputTemplate.SubTitle} subTitle={Locale.Settings.InputTemplate.SubTitle}
> >
<input <input
aria-label={Locale.Settings.InputTemplate.Title}
type="text" type="text"
value={props.modelConfig.template} value={props.modelConfig.template}
onChange={(e) => onChange={(e) =>
@ -175,6 +193,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.HistoryCount.SubTitle} subTitle={Locale.Settings.HistoryCount.SubTitle}
> >
<InputRange <InputRange
aria={Locale.Settings.HistoryCount.Title}
title={props.modelConfig.historyMessageCount.toString()} title={props.modelConfig.historyMessageCount.toString()}
value={props.modelConfig.historyMessageCount} value={props.modelConfig.historyMessageCount}
min="0" min="0"
@ -193,6 +212,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.CompressThreshold.SubTitle} subTitle={Locale.Settings.CompressThreshold.SubTitle}
> >
<input <input
aria-label={Locale.Settings.CompressThreshold.Title}
type="number" type="number"
min={500} min={500}
max={4000} max={4000}
@ -208,6 +228,7 @@ export function ModelConfigList(props: {
</ListItem> </ListItem>
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}> <ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
<input <input
aria-label={Locale.Memory.Title}
type="checkbox" type="checkbox"
checked={props.modelConfig.sendMemory} checked={props.modelConfig.sendMemory}
onChange={(e) => onChange={(e) =>
@ -217,6 +238,31 @@ export function ModelConfigList(props: {
} }
></input> ></input>
</ListItem> </ListItem>
<ListItem
title={Locale.Settings.CompressModel.Title}
subTitle={Locale.Settings.CompressModel.SubTitle}
>
<Select
className={styles["select-compress-model"]}
aria-label={Locale.Settings.CompressModel.Title}
value={compressModelValue}
onChange={(e) => {
const [model, providerName] = e.currentTarget.value.split("@");
props.updateConfig((config) => {
config.compressModel = ModalConfigValidator.model(model);
config.compressProviderName = providerName as ServiceProvider;
});
}}
>
{allModels
.filter((v) => v.available)
.map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName})
</option>
))}
</Select>
</ListItem>
</> </>
); );
} }

View File

@ -0,0 +1,38 @@
.plugin-title {
font-weight: bolder;
font-size: 16px;
margin: 10px 0;
}
.plugin-content {
font-size: 14px;
font-family: inherit;
pre code {
max-height: 240px;
overflow-y: auto;
white-space: pre-wrap;
min-width: 280px;
}
}
.plugin-schema {
display: flex;
justify-content: flex-end;
flex-direction: row;
input {
margin-right: 20px;
@media screen and (max-width: 600px) {
margin-right: 0px;
}
}
@media screen and (max-width: 600px) {
flex-direction: column;
gap: 5px;
button {
padding: 10px;
}
}
}

366
app/components/plugin.tsx Normal file
View File

@ -0,0 +1,366 @@
import { useDebouncedCallback } from "use-debounce";
import OpenAPIClientAxios from "openapi-client-axios";
import yaml from "js-yaml";
import { PLUGINS_REPO_URL } from "../constant";
import { IconButton } from "./button";
import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
import pluginStyles from "./plugin.module.scss";
import EditIcon from "../icons/edit.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import ConfirmIcon from "../icons/confirm.svg";
import ReloadIcon from "../icons/reload.svg";
import GithubIcon from "../icons/github.svg";
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
import {
PasswordInput,
List,
ListItem,
Modal,
showConfirm,
showToast,
} from "./ui-lib";
import Locale from "../locales";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
export function PluginPage() {
const navigate = useNavigate();
const pluginStore = usePluginStore();
const allPlugins = pluginStore.getAll();
const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
const [searchText, setSearchText] = useState("");
const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
// refactored already, now it accurate
const onSearch = (text: string) => {
setSearchText(text);
if (text.length > 0) {
const result = allPlugins.filter(
(m) => m?.title.toLowerCase().includes(text.toLowerCase()),
);
setSearchPlugins(result);
} else {
setSearchPlugins(allPlugins);
}
};
const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
const editingPlugin = pluginStore.get(editingPluginId);
const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
const closePluginModal = () => setEditingPluginId(undefined);
const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
const content = e.target.innerText;
try {
const api = new OpenAPIClientAxios({
definition: yaml.load(content) as any,
});
api
.init()
.then(() => {
if (content != editingPlugin.content) {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.content = content;
const tool = FunctionToolService.add(plugin, true);
plugin.title = tool.api.definition.info.title;
plugin.version = tool.api.definition.info.version;
});
}
})
.catch((e) => {
console.error(e);
showToast(Locale.Plugin.EditModal.Error);
});
} catch (e) {
console.error(e);
showToast(Locale.Plugin.EditModal.Error);
}
}, 100).bind(null, editingPlugin);
const [loadUrl, setLoadUrl] = useState<string>("");
const loadFromUrl = (loadUrl: string) =>
fetch(loadUrl)
.catch((e) => {
const p = new URL(loadUrl);
return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
headers: {
"X-Base-URL": p.origin,
},
});
})
.then((res) => res.text())
.then((content) => {
try {
return JSON.stringify(JSON.parse(content), null, " ");
} catch (e) {
return content;
}
})
.then((content) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.content = content;
const tool = FunctionToolService.add(plugin, true);
plugin.title = tool.api.definition.info.title;
plugin.version = tool.api.definition.info.version;
});
})
.catch((e) => {
showToast(Locale.Plugin.EditModal.Error);
});
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
{Locale.Plugin.Page.Title}
</div>
<div className="window-header-submai-title">
{Locale.Plugin.Page.SubTitle(plugins.length)}
</div>
</div>
<div className="window-actions">
<div className="window-action-button">
<a
href={PLUGINS_REPO_URL}
target="_blank"
rel="noopener noreferrer"
>
<IconButton icon={<GithubIcon />} bordered />
</a>
</div>
<div className="window-action-button">
<IconButton
icon={<CloseIcon />}
bordered
onClick={() => navigate(-1)}
/>
</div>
</div>
</div>
<div className={styles["mask-page-body"]}>
<div className={styles["mask-filter"]}>
<input
type="text"
className={styles["search-bar"]}
placeholder={Locale.Plugin.Page.Search}
autoFocus
onInput={(e) => onSearch(e.currentTarget.value)}
/>
<IconButton
className={styles["mask-create"]}
icon={<AddIcon />}
text={Locale.Plugin.Page.Create}
bordered
onClick={() => {
const createdPlugin = pluginStore.create();
setEditingPluginId(createdPlugin.id);
}}
/>
</div>
<div>
{plugins.length == 0 && (
<div
style={{
display: "flex",
margin: "60px auto",
alignItems: "center",
justifyContent: "center",
}}
>
{Locale.Plugin.Page.Find}
<a
href={PLUGINS_REPO_URL}
target="_blank"
rel="noopener noreferrer"
style={{ marginLeft: 16 }}
>
<IconButton icon={<GithubIcon />} bordered />
</a>
</div>
)}
{plugins.map((m) => (
<div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}>
<div className={styles["mask-icon"]}></div>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>
{m.title}@<small>{m.version}</small>
</div>
<div className={styles["mask-info"] + " one-line"}>
{Locale.Plugin.Item.Info(
FunctionToolService.add(m).length,
)}
</div>
</div>
</div>
<div className={styles["mask-actions"]}>
<IconButton
icon={<EditIcon />}
text={Locale.Plugin.Item.Edit}
onClick={() => setEditingPluginId(m.id)}
/>
{!m.builtin && (
<IconButton
icon={<DeleteIcon />}
text={Locale.Plugin.Item.Delete}
onClick={async () => {
if (
await showConfirm(Locale.Plugin.Item.DeleteConfirm)
) {
pluginStore.delete(m.id);
}
}}
/>
)}
</div>
</div>
))}
</div>
</div>
</div>
{editingPlugin && (
<div className="modal-mask">
<Modal
title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)}
onClose={closePluginModal}
actions={[
<IconButton
icon={<ConfirmIcon />}
text={Locale.UI.Confirm}
key="export"
bordered
onClick={() => setEditingPluginId("")}
/>,
]}
>
<List>
<ListItem title={Locale.Plugin.EditModal.Auth}>
<select
value={editingPlugin?.authType}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authType = e.target.value;
});
}}
>
<option value="">{Locale.Plugin.Auth.None}</option>
<option value="bearer">{Locale.Plugin.Auth.Bearer}</option>
<option value="basic">{Locale.Plugin.Auth.Basic}</option>
<option value="custom">{Locale.Plugin.Auth.Custom}</option>
</select>
</ListItem>
{["bearer", "basic", "custom"].includes(
editingPlugin.authType as string,
) && (
<ListItem title={Locale.Plugin.Auth.Location}>
<select
value={editingPlugin?.authLocation}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authLocation = e.target.value;
});
}}
>
<option value="header">
{Locale.Plugin.Auth.LocationHeader}
</option>
<option value="query">
{Locale.Plugin.Auth.LocationQuery}
</option>
<option value="body">
{Locale.Plugin.Auth.LocationBody}
</option>
</select>
</ListItem>
)}
{editingPlugin.authType == "custom" && (
<ListItem title={Locale.Plugin.Auth.CustomHeader}>
<input
type="text"
value={editingPlugin?.authHeader}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authHeader = e.target.value;
});
}}
></input>
</ListItem>
)}
{["bearer", "basic", "custom"].includes(
editingPlugin.authType as string,
) && (
<ListItem title={Locale.Plugin.Auth.Token}>
<PasswordInput
type="text"
value={editingPlugin?.authToken}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authToken = e.currentTarget.value;
});
}}
></PasswordInput>
</ListItem>
)}
</List>
<List>
<ListItem title={Locale.Plugin.EditModal.Content}>
<div className={pluginStyles["plugin-schema"]}>
<input
type="text"
style={{ minWidth: 200 }}
onInput={(e) => setLoadUrl(e.currentTarget.value)}
></input>
<IconButton
icon={<ReloadIcon />}
text={Locale.Plugin.EditModal.Load}
bordered
onClick={() => loadFromUrl(loadUrl)}
/>
</div>
</ListItem>
<ListItem
subTitle={
<div
className={`markdown-body ${pluginStyles["plugin-content"]}`}
dir="auto"
>
<pre>
<code
contentEditable={true}
dangerouslySetInnerHTML={{
__html: editingPlugin.content,
}}
onBlur={onChangePlugin}
></code>
</pre>
</div>
}
></ListItem>
{editingPluginTool?.tools.map((tool, index) => (
<ListItem
key={index}
title={tool?.function?.name}
subTitle={tool?.function?.description}
/>
))}
</List>
</Modal>
</div>
)}
</ErrorBoundary>
);
}

View File

@ -0,0 +1,2 @@
export * from "./sd";
export * from "./sd-panel";

View File

@ -0,0 +1,45 @@
.ctrl-param-item {
display: flex;
justify-content: space-between;
min-height: 40px;
padding: 10px 0;
animation: slide-in ease 0.6s;
flex-direction: column;
.ctrl-param-item-header {
display: flex;
align-items: center;
.ctrl-param-item-title {
font-size: 14px;
font-weight: bolder;
margin-bottom: 5px;
}
}
.ctrl-param-item-sub-title {
font-size: 12px;
font-weight: normal;
margin-top: 3px;
}
textarea {
appearance: none;
border-radius: 10px;
border: var(--border-in-light);
min-height: 36px;
box-sizing: border-box;
background: var(--white);
color: var(--black);
padding: 0 10px;
max-width: 50%;
font-family: inherit;
}
}
.ai-models {
button {
margin-bottom: 10px;
padding: 10px;
width: 100%;
}
}

View File

@ -0,0 +1,320 @@
import styles from "./sd-panel.module.scss";
import React from "react";
import { Select } from "@/app/components/ui-lib";
import { IconButton } from "@/app/components/button";
import Locale from "@/app/locales";
import { useSdStore } from "@/app/store/sd";
export const params = [
{
name: Locale.SdPanel.Prompt,
value: "prompt",
type: "textarea",
placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
required: true,
},
{
name: Locale.SdPanel.ModelVersion,
value: "model",
type: "select",
default: "sd3-medium",
support: ["sd3"],
options: [
{ name: "SD3 Medium", value: "sd3-medium" },
{ name: "SD3 Large", value: "sd3-large" },
{ name: "SD3 Large Turbo", value: "sd3-large-turbo" },
],
},
{
name: Locale.SdPanel.NegativePrompt,
value: "negative_prompt",
type: "textarea",
placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
},
{
name: Locale.SdPanel.AspectRatio,
value: "aspect_ratio",
type: "select",
default: "1:1",
options: [
{ name: "1:1", value: "1:1" },
{ name: "16:9", value: "16:9" },
{ name: "21:9", value: "21:9" },
{ name: "2:3", value: "2:3" },
{ name: "3:2", value: "3:2" },
{ name: "4:5", value: "4:5" },
{ name: "5:4", value: "5:4" },
{ name: "9:16", value: "9:16" },
{ name: "9:21", value: "9:21" },
],
},
{
name: Locale.SdPanel.ImageStyle,
value: "style",
type: "select",
default: "3d-model",
support: ["core"],
options: [
{ name: Locale.SdPanel.Styles.D3Model, value: "3d-model" },
{ name: Locale.SdPanel.Styles.AnalogFilm, value: "analog-film" },
{ name: Locale.SdPanel.Styles.Anime, value: "anime" },
{ name: Locale.SdPanel.Styles.Cinematic, value: "cinematic" },
{ name: Locale.SdPanel.Styles.ComicBook, value: "comic-book" },
{ name: Locale.SdPanel.Styles.DigitalArt, value: "digital-art" },
{ name: Locale.SdPanel.Styles.Enhance, value: "enhance" },
{ name: Locale.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
{ name: Locale.SdPanel.Styles.Isometric, value: "isometric" },
{ name: Locale.SdPanel.Styles.LineArt, value: "line-art" },
{ name: Locale.SdPanel.Styles.LowPoly, value: "low-poly" },
{
name: Locale.SdPanel.Styles.ModelingCompound,
value: "modeling-compound",
},
{ name: Locale.SdPanel.Styles.NeonPunk, value: "neon-punk" },
{ name: Locale.SdPanel.Styles.Origami, value: "origami" },
{ name: Locale.SdPanel.Styles.Photographic, value: "photographic" },
{ name: Locale.SdPanel.Styles.PixelArt, value: "pixel-art" },
{ name: Locale.SdPanel.Styles.TileTexture, value: "tile-texture" },
],
},
{
name: "Seed",
value: "seed",
type: "number",
default: 0,
min: 0,
max: 4294967294,
},
{
name: Locale.SdPanel.OutFormat,
value: "output_format",
type: "select",
default: "png",
options: [
{ name: "PNG", value: "png" },
{ name: "JPEG", value: "jpeg" },
{ name: "WebP", value: "webp" },
],
},
];
const sdCommonParams = (model: string, data: any) => {
return params.filter((item) => {
return !(item.support && !item.support.includes(model));
});
};
export const models = [
{
name: "Stable Image Ultra",
value: "ultra",
params: (data: any) => sdCommonParams("ultra", data),
},
{
name: "Stable Image Core",
value: "core",
params: (data: any) => sdCommonParams("core", data),
},
{
name: "Stable Diffusion 3",
value: "sd3",
params: (data: any) => {
return sdCommonParams("sd3", data).filter((item) => {
return !(
data.model === "sd3-large-turbo" && item.value == "negative_prompt"
);
});
},
},
];
export function ControlParamItem(props: {
title: string;
subTitle?: string;
required?: boolean;
children?: JSX.Element | JSX.Element[];
className?: string;
}) {
return (
<div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
<div className={styles["ctrl-param-item-header"]}>
<div className={styles["ctrl-param-item-title"]}>
<div>
{props.title}
{props.required && <span style={{ color: "red" }}>*</span>}
</div>
</div>
</div>
{props.children}
{props.subTitle && (
<div className={styles["ctrl-param-item-sub-title"]}>
{props.subTitle}
</div>
)}
</div>
);
}
export function ControlParam(props: {
columns: any[];
data: any;
onChange: (field: string, val: any) => void;
}) {
return (
<>
{props.columns?.map((item) => {
let element: null | JSX.Element;
switch (item.type) {
case "textarea":
element = (
<ControlParamItem
title={item.name}
subTitle={item.sub}
required={item.required}
>
<textarea
rows={item.rows || 3}
style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
placeholder={item.placeholder}
onChange={(e) => {
props.onChange(item.value, e.currentTarget.value);
}}
value={props.data[item.value]}
></textarea>
</ControlParamItem>
);
break;
case "select":
element = (
<ControlParamItem
title={item.name}
subTitle={item.sub}
required={item.required}
>
<Select
aria-label={item.name}
value={props.data[item.value]}
onChange={(e) => {
props.onChange(item.value, e.currentTarget.value);
}}
>
{item.options.map((opt: any) => {
return (
<option value={opt.value} key={opt.value}>
{opt.name}
</option>
);
})}
</Select>
</ControlParamItem>
);
break;
case "number":
element = (
<ControlParamItem
title={item.name}
subTitle={item.sub}
required={item.required}
>
<input
aria-label={item.name}
type="number"
min={item.min}
max={item.max}
value={props.data[item.value] || 0}
onChange={(e) => {
props.onChange(item.value, parseInt(e.currentTarget.value));
}}
/>
</ControlParamItem>
);
break;
default:
element = (
<ControlParamItem
title={item.name}
subTitle={item.sub}
required={item.required}
>
<input
aria-label={item.name}
type="text"
value={props.data[item.value]}
style={{ maxWidth: "100%", width: "100%" }}
onChange={(e) => {
props.onChange(item.value, e.currentTarget.value);
}}
/>
</ControlParamItem>
);
}
return <div key={item.value}>{element}</div>;
})}
</>
);
}
export const getModelParamBasicData = (
columns: any[],
data: any,
clearText?: boolean,
) => {
const newParams: any = {};
columns.forEach((item: any) => {
if (clearText && ["text", "textarea", "number"].includes(item.type)) {
newParams[item.value] = item.default || "";
} else {
// @ts-ignore
newParams[item.value] = data[item.value] || item.default || "";
}
});
return newParams;
};
export const getParams = (model: any, params: any) => {
return models.find((m) => m.value === model.value)?.params(params) || [];
};
export function SdPanel() {
const sdStore = useSdStore();
const currentModel = sdStore.currentModel;
const setCurrentModel = sdStore.setCurrentModel;
const params = sdStore.currentParams;
const setParams = sdStore.setCurrentParams;
const handleValueChange = (field: string, val: any) => {
setParams({
...params,
[field]: val,
});
};
const handleModelChange = (model: any) => {
setCurrentModel(model);
setParams(getModelParamBasicData(model.params({}), params));
};
return (
<>
<ControlParamItem title={Locale.SdPanel.AIModel}>
<div className={styles["ai-models"]}>
{models.map((item) => {
return (
<IconButton
text={item.name}
key={item.value}
type={currentModel.value == item.value ? "primary" : null}
shadow
onClick={() => handleModelChange(item)}
/>
);
})}
</div>
</ControlParamItem>
<ControlParam
columns={getParams?.(currentModel, params) as any[]}
data={params}
onChange={handleValueChange}
></ControlParam>
</>
);
}

View File

@ -0,0 +1,140 @@
import { IconButton } from "@/app/components/button";
import GithubIcon from "@/app/icons/github.svg";
import SDIcon from "@/app/icons/sd.svg";
import ReturnIcon from "@/app/icons/return.svg";
import HistoryIcon from "@/app/icons/history.svg";
import Locale from "@/app/locales";
import { Path, REPO_URL } from "@/app/constant";
import { useNavigate } from "react-router-dom";
import dynamic from "next/dynamic";
import {
SideBarContainer,
SideBarBody,
SideBarHeader,
SideBarTail,
useDragSideBar,
useHotKey,
} from "@/app/components/sidebar";
import { getParams, getModelParamBasicData } from "./sd-panel";
import { useSdStore } from "@/app/store/sd";
import { showToast } from "@/app/components/ui-lib";
import { useMobileScreen } from "@/app/utils";
const SdPanel = dynamic(
async () => (await import("@/app/components/sd")).SdPanel,
{
loading: () => null,
},
);
export function SideBar(props: { className?: string }) {
useHotKey();
const isMobileScreen = useMobileScreen();
const { onDragStart, shouldNarrow } = useDragSideBar();
const navigate = useNavigate();
const sdStore = useSdStore();
const currentModel = sdStore.currentModel;
const params = sdStore.currentParams;
const setParams = sdStore.setCurrentParams;
const handleSubmit = () => {
const columns = getParams?.(currentModel, params);
const reqParams: any = {};
for (let i = 0; i < columns.length; i++) {
const item = columns[i];
reqParams[item.value] = params[item.value] ?? null;
if (item.required) {
if (!reqParams[item.value]) {
showToast(Locale.SdPanel.ParamIsRequired(item.name));
return;
}
}
}
let data: any = {
model: currentModel.value,
model_name: currentModel.name,
status: "wait",
params: reqParams,
created_at: new Date().toLocaleString(),
img_data: "",
};
sdStore.sendTask(data, () => {
setParams(getModelParamBasicData(columns, params, true));
navigate(Path.SdNew);
});
};
return (
<SideBarContainer
onDragStart={onDragStart}
shouldNarrow={shouldNarrow}
{...props}
>
{isMobileScreen ? (
<div
className="window-header"
data-tauri-drag-region
style={{
paddingLeft: 0,
paddingRight: 0,
}}
>
<div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<ReturnIcon />}
bordered
title={Locale.Sd.Actions.ReturnHome}
onClick={() => navigate(Path.Home)}
/>
</div>
</div>
<SDIcon width={50} height={50} />
<div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<HistoryIcon />}
bordered
title={Locale.Sd.Actions.History}
onClick={() => navigate(Path.SdNew)}
/>
</div>
</div>
</div>
) : (
<SideBarHeader
title={
<IconButton
icon={<ReturnIcon />}
bordered
title={Locale.Sd.Actions.ReturnHome}
onClick={() => navigate(Path.Home)}
/>
}
logo={<SDIcon width={38} height={"100%"} />}
></SideBarHeader>
)}
<SideBarBody>
<SdPanel />
</SideBarBody>
<SideBarTail
primaryAction={
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
<IconButton icon={<GithubIcon />} shadow />
</a>
}
secondaryAction={
<IconButton
text={Locale.SdPanel.Submit}
type="primary"
shadow
onClick={handleSubmit}
></IconButton>
}
/>
</SideBarContainer>
);
}

View File

@ -0,0 +1,53 @@
.sd-img-list{
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.sd-img-item{
width: 48%;
.sd-img-item-info{
flex:1;
width: 100%;
overflow: hidden;
user-select: text;
p{
margin: 6px;
font-size: 12px;
}
.line-1{
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.pre-img{
display: flex;
width: 130px;
justify-content: center;
align-items: center;
background-color: var(--second);
border-radius: 10px;
}
.img{
width: 130px;
height: 130px;
border-radius: 10px;
overflow: hidden;
cursor: pointer;
transition: all .3s;
&:hover{
opacity: .7;
}
}
&:not(:last-child){
margin-bottom: 20px;
}
}
}
@media only screen and (max-width: 600px) {
.sd-img-list{
.sd-img-item{
width: 100%;
}
}
}

336
app/components/sd/sd.tsx Normal file
View File

@ -0,0 +1,336 @@
import chatStyles from "@/app/components/chat.module.scss";
import styles from "@/app/components/sd/sd.module.scss";
import homeStyles from "@/app/components/home.module.scss";
import { IconButton } from "@/app/components/button";
import ReturnIcon from "@/app/icons/return.svg";
import Locale from "@/app/locales";
import { Path } from "@/app/constant";
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
copyToClipboard,
getMessageTextContent,
useMobileScreen,
} from "@/app/utils";
import { useNavigate, useLocation } from "react-router-dom";
import { useAppConfig } from "@/app/store";
import MinIcon from "@/app/icons/min.svg";
import MaxIcon from "@/app/icons/max.svg";
import { getClientConfig } from "@/app/config/client";
import { ChatAction } from "@/app/components/chat";
import DeleteIcon from "@/app/icons/clear.svg";
import CopyIcon from "@/app/icons/copy.svg";
import PromptIcon from "@/app/icons/prompt.svg";
import ResetIcon from "@/app/icons/reload.svg";
import { useSdStore } from "@/app/store/sd";
import LoadingIcon from "@/app/icons/three-dots.svg";
import ErrorIcon from "@/app/icons/delete.svg";
import SDIcon from "@/app/icons/sd.svg";
import { Property } from "csstype";
import {
showConfirm,
showImageModal,
showModal,
} from "@/app/components/ui-lib";
import { removeImage } from "@/app/utils/chat";
import { SideBar } from "./sd-sidebar";
import { WindowContent } from "@/app/components/home";
import { params } from "./sd-panel";
function getSdTaskStatus(item: any) {
let s: string;
let color: Property.Color | undefined = undefined;
switch (item.status) {
case "success":
s = Locale.Sd.Status.Success;
color = "green";
break;
case "error":
s = Locale.Sd.Status.Error;
color = "red";
break;
case "wait":
s = Locale.Sd.Status.Wait;
color = "yellow";
break;
case "running":
s = Locale.Sd.Status.Running;
color = "blue";
break;
default:
s = item.status.toUpperCase();
}
return (
<p className={styles["line-1"]} title={item.error} style={{ color: color }}>
<span>
{Locale.Sd.Status.Name}: {s}
</span>
{item.status === "error" && (
<span
className="clickable"
onClick={() => {
showModal({
title: Locale.Sd.Detail,
children: (
<div style={{ color: color, userSelect: "text" }}>
{item.error}
</div>
),
});
}}
>
- {item.error}
</span>
)}
</p>
);
}
export function Sd() {
const isMobileScreen = useMobileScreen();
const navigate = useNavigate();
const location = useLocation();
const clientConfig = useMemo(() => getClientConfig(), []);
const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
const config = useAppConfig();
const scrollRef = useRef<HTMLDivElement>(null);
const sdStore = useSdStore();
const [sdImages, setSdImages] = useState(sdStore.draw);
const isSd = location.pathname === Path.Sd;
useEffect(() => {
setSdImages(sdStore.draw);
}, [sdStore.currentId]);
return (
<>
<SideBar className={isSd ? homeStyles["sidebar-show"] : ""} />
<WindowContent>
<div className={chatStyles.chat} key={"1"}>
<div className="window-header" data-tauri-drag-region>
{isMobileScreen && (
<div className="window-actions">
<div className={"window-action-button"}>
<IconButton
icon={<ReturnIcon />}
bordered
title={Locale.Chat.Actions.ChatList}
onClick={() => navigate(Path.Sd)}
/>
</div>
</div>
)}
<div
className={`window-header-title ${chatStyles["chat-body-title"]}`}
>
<div className={`window-header-main-title`}>Stability AI</div>
<div className="window-header-sub-title">
{Locale.Sd.SubTitle(sdImages.length || 0)}
</div>
</div>
<div className="window-actions">
{showMaxIcon && (
<div className="window-action-button">
<IconButton
aria={Locale.Chat.Actions.FullScreen}
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
bordered
onClick={() => {
config.update(
(config) => (config.tightBorder = !config.tightBorder),
);
}}
/>
</div>
)}
{isMobileScreen && <SDIcon width={50} height={50} />}
</div>
</div>
<div className={chatStyles["chat-body"]} ref={scrollRef}>
<div className={styles["sd-img-list"]}>
{sdImages.length > 0 ? (
sdImages.map((item: any) => {
return (
<div
key={item.id}
style={{ display: "flex" }}
className={styles["sd-img-item"]}
>
{item.status === "success" ? (
<img
className={styles["img"]}
src={item.img_data}
alt={item.id}
onClick={(e) =>
showImageModal(
item.img_data,
true,
isMobileScreen
? { width: "100%", height: "fit-content" }
: { maxWidth: "100%", maxHeight: "100%" },
isMobileScreen
? { width: "100%", height: "fit-content" }
: { width: "100%", height: "100%" },
)
}
/>
) : item.status === "error" ? (
<div className={styles["pre-img"]}>
<ErrorIcon />
</div>
) : (
<div className={styles["pre-img"]}>
<LoadingIcon />
</div>
)}
<div
style={{ marginLeft: "10px" }}
className={styles["sd-img-item-info"]}
>
<p className={styles["line-1"]}>
{Locale.SdPanel.Prompt}:{" "}
<span
className="clickable"
title={item.params.prompt}
onClick={() => {
showModal({
title: Locale.Sd.Detail,
children: (
<div style={{ userSelect: "text" }}>
{item.params.prompt}
</div>
),
});
}}
>
{item.params.prompt}
</span>
</p>
<p>
{Locale.SdPanel.AIModel}: {item.model_name}
</p>
{getSdTaskStatus(item)}
<p>{item.created_at}</p>
<div className={chatStyles["chat-message-actions"]}>
<div className={chatStyles["chat-input-actions"]}>
<ChatAction
text={Locale.Sd.Actions.Params}
icon={<PromptIcon />}
onClick={() => {
showModal({
title: Locale.Sd.GenerateParams,
children: (
<div style={{ userSelect: "text" }}>
{Object.keys(item.params).map((key) => {
let label = key;
let value = item.params[key];
switch (label) {
case "prompt":
label = Locale.SdPanel.Prompt;
break;
case "negative_prompt":
label =
Locale.SdPanel.NegativePrompt;
break;
case "aspect_ratio":
label = Locale.SdPanel.AspectRatio;
break;
case "seed":
label = "Seed";
value = value || 0;
break;
case "output_format":
label = Locale.SdPanel.OutFormat;
value = value?.toUpperCase();
break;
case "style":
label = Locale.SdPanel.ImageStyle;
value = params
.find(
(item) =>
item.value === "style",
)
?.options?.find(
(item) => item.value === value,
)?.name;
break;
default:
break;
}
return (
<div
key={key}
style={{ margin: "10px" }}
>
<strong>{label}: </strong>
{value}
</div>
);
})}
</div>
),
});
}}
/>
<ChatAction
text={Locale.Sd.Actions.Copy}
icon={<CopyIcon />}
onClick={() =>
copyToClipboard(
getMessageTextContent({
role: "user",
content: item.params.prompt,
}),
)
}
/>
<ChatAction
text={Locale.Sd.Actions.Retry}
icon={<ResetIcon />}
onClick={() => {
const reqData = {
model: item.model,
model_name: item.model_name,
status: "wait",
params: { ...item.params },
created_at: new Date().toLocaleString(),
img_data: "",
};
sdStore.sendTask(reqData);
}}
/>
<ChatAction
text={Locale.Sd.Actions.Delete}
icon={<DeleteIcon />}
onClick={async () => {
if (
await showConfirm(Locale.Sd.Danger.Delete)
) {
// remove img_data + remove item in list
removeImage(item.img_data).finally(() => {
sdStore.draw = sdImages.filter(
(i: any) => i.id !== item.id,
);
sdStore.getNextId();
});
}
}}
/>
</div>
</div>
</div>
</div>
);
})
) : (
<div>{Locale.Sd.EmptyRecord}</div>
)}
</div>
</div>
</div>
</WindowContent>
</>
);
}

View File

@ -0,0 +1,167 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
import { useNavigate } from "react-router-dom";
import { IconButton } from "./button";
import CloseIcon from "../icons/close.svg";
import EyeIcon from "../icons/eye.svg";
import Locale from "../locales";
import { Path } from "../constant";
import { useChatStore } from "../store";
type Item = {
id: number;
name: string;
content: string;
};
export function SearchChatPage() {
const navigate = useNavigate();
const chatStore = useChatStore();
const sessions = chatStore.sessions;
const selectSession = chatStore.selectSession;
const [searchResults, setSearchResults] = useState<Item[]>([]);
const previousValueRef = useRef<string>("");
const searchInputRef = useRef<HTMLInputElement>(null);
const doSearch = useCallback((text: string) => {
const lowerCaseText = text.toLowerCase();
const results: Item[] = [];
sessions.forEach((session, index) => {
const fullTextContents: string[] = [];
session.messages.forEach((message) => {
const content = message.content as string;
if (!content.toLowerCase || content === "") return;
const lowerCaseContent = content.toLowerCase();
// full text search
let pos = lowerCaseContent.indexOf(lowerCaseText);
while (pos !== -1) {
const start = Math.max(0, pos - 35);
const end = Math.min(content.length, pos + lowerCaseText.length + 35);
fullTextContents.push(content.substring(start, end));
pos = lowerCaseContent.indexOf(
lowerCaseText,
pos + lowerCaseText.length,
);
}
});
if (fullTextContents.length > 0) {
results.push({
id: index,
name: session.topic,
content: fullTextContents.join("... "), // concat content with...
});
}
});
// sort by length of matching content
results.sort((a, b) => b.content.length - a.content.length);
return results;
}, []);
useEffect(() => {
const intervalId = setInterval(() => {
if (searchInputRef.current) {
const currentValue = searchInputRef.current.value;
if (currentValue !== previousValueRef.current) {
if (currentValue.length > 0) {
const result = doSearch(currentValue);
setSearchResults(result);
}
previousValueRef.current = currentValue;
}
}
}, 1000);
// Cleanup the interval on component unmount
return () => clearInterval(intervalId);
}, [doSearch]);
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
{/* header */}
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
{Locale.SearchChat.Page.Title}
</div>
<div className="window-header-submai-title">
{Locale.SearchChat.Page.SubTitle(searchResults.length)}
</div>
</div>
<div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<CloseIcon />}
bordered
onClick={() => navigate(-1)}
/>
</div>
</div>
</div>
<div className={styles["mask-page-body"]}>
<div className={styles["mask-filter"]}>
{/**搜索输入框 */}
<input
type="text"
className={styles["search-bar"]}
placeholder={Locale.SearchChat.Page.Search}
autoFocus
ref={searchInputRef}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const searchText = e.currentTarget.value;
if (searchText.length > 0) {
const result = doSearch(searchText);
setSearchResults(result);
}
}
}}
/>
</div>
<div>
{searchResults.map((item) => (
<div
className={styles["mask-item"]}
key={item.id}
onClick={() => {
navigate(Path.Chat);
selectSession(item.id);
}}
style={{ cursor: "pointer" }}
>
{/** 搜索匹配的文本 */}
<div className={styles["mask-header"]}>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{item.name}</div>
{item.content.slice(0, 70)}
</div>
</div>
{/** 操作按钮 */}
<div className={styles["mask-actions"]}>
<IconButton
icon={<EyeIcon />}
text={Locale.SearchChat.Item.View}
/>
</div>
</div>
))}
</div>
</div>
</div>
</ErrorBoundary>
);
}

View File

@ -72,3 +72,9 @@
} }
} }
} }
.subtitle-button {
button {
overflow:visible ;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useMemo } from "react"; import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
@ -7,11 +7,10 @@ import SettingsIcon from "../icons/settings.svg";
import GithubIcon from "../icons/github.svg"; import GithubIcon from "../icons/github.svg";
import ChatGptIcon from "../icons/chatgpt.svg"; import ChatGptIcon from "../icons/chatgpt.svg";
import AddIcon from "../icons/add.svg"; import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg"; import DeleteIcon from "../icons/delete.svg";
import MaskIcon from "../icons/mask.svg"; import MaskIcon from "../icons/mask.svg";
import PluginIcon from "../icons/plugin.svg";
import DragIcon from "../icons/drag.svg"; import DragIcon from "../icons/drag.svg";
import DiscoveryIcon from "../icons/discovery.svg";
import Locale from "../locales"; import Locale from "../locales";
@ -23,19 +22,20 @@ import {
MIN_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH,
NARROW_SIDEBAR_WIDTH, NARROW_SIDEBAR_WIDTH,
Path, Path,
PLUGINS,
REPO_URL, REPO_URL,
} from "../constant"; } from "../constant";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { isIOS, useMobileScreen } from "../utils"; import { isIOS, useMobileScreen } from "../utils";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { showConfirm, showToast } from "./ui-lib"; import { showConfirm, Selector } from "./ui-lib";
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => null, loading: () => null,
}); });
function useHotKey() { export function useHotKey() {
const chatStore = useChatStore(); const chatStore = useChatStore();
useEffect(() => { useEffect(() => {
@ -54,7 +54,7 @@ function useHotKey() {
}); });
} }
function useDragSideBar() { export function useDragSideBar() {
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x); const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
const config = useAppConfig(); const config = useAppConfig();
@ -127,25 +127,21 @@ function useDragSideBar() {
shouldNarrow, shouldNarrow,
}; };
} }
export function SideBarContainer(props: {
export function SideBar(props: { className?: string }) { children: React.ReactNode;
const chatStore = useChatStore(); onDragStart: (e: MouseEvent) => void;
shouldNarrow: boolean;
// drag side bar className?: string;
const { onDragStart, shouldNarrow } = useDragSideBar(); }) {
const navigate = useNavigate();
const config = useAppConfig();
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
const isIOSMobile = useMemo( const isIOSMobile = useMemo(
() => isIOS() && isMobileScreen, () => isIOS() && isMobileScreen,
[isMobileScreen], [isMobileScreen],
); );
const { children, className, onDragStart, shouldNarrow } = props;
useHotKey();
return ( return (
<div <div
className={`${styles.sidebar} ${props.className} ${ className={`${styles.sidebar} ${className} ${
shouldNarrow && styles["narrow-sidebar"] shouldNarrow && styles["narrow-sidebar"]
}`} }`}
style={{ style={{
@ -153,43 +149,132 @@ export function SideBar(props: { className?: string }) {
transition: isMobileScreen && isIOSMobile ? "none" : undefined, transition: isMobileScreen && isIOSMobile ? "none" : undefined,
}} }}
> >
<div className={styles["sidebar-header"]} data-tauri-drag-region> {children}
<div className={styles["sidebar-title"]} data-tauri-drag-region>
NextChat
</div>
<div className={styles["sidebar-sub-title"]}>
Build your own AI assistant.
</div>
<div className={styles["sidebar-logo"] + " no-dark"}>
<ChatGptIcon />
</div>
</div>
<div className={styles["sidebar-header-bar"]}>
<IconButton
icon={<MaskIcon />}
text={shouldNarrow ? undefined : Locale.Mask.Name}
className={styles["sidebar-bar-button"]}
onClick={() => {
if (config.dontShowMaskSplashScreen !== true) {
navigate(Path.NewChat, { state: { fromHome: true } });
} else {
navigate(Path.Masks, { state: { fromHome: true } });
}
}}
shadow
/>
<IconButton
icon={<PluginIcon />}
text={shouldNarrow ? undefined : Locale.Plugin.Name}
className={styles["sidebar-bar-button"]}
onClick={() => showToast(Locale.WIP)}
shadow
/>
</div>
<div <div
className={styles["sidebar-body"]} className={styles["sidebar-drag"]}
onPointerDown={(e) => onDragStart(e as any)}
>
<DragIcon />
</div>
</div>
);
}
export function SideBarHeader(props: {
title?: string | React.ReactNode;
subTitle?: string | React.ReactNode;
logo?: React.ReactNode;
children?: React.ReactNode;
shouldNarrow?: boolean;
}) {
const { title, subTitle, logo, children, shouldNarrow } = props;
return (
<Fragment>
<div
className={`${styles["sidebar-header"]} ${
shouldNarrow ? styles["sidebar-header-narrow"] : ""
}`}
data-tauri-drag-region
>
<div className={styles["sidebar-title-container"]}>
<div className={styles["sidebar-title"]} data-tauri-drag-region>
{title}
</div>
<div className={styles["sidebar-sub-title"]}>{subTitle}</div>
</div>
<div className={styles["sidebar-logo"] + " no-dark"}>{logo}</div>
</div>
{children}
</Fragment>
);
}
export function SideBarBody(props: {
children: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
}) {
const { onClick, children } = props;
return (
<div className={styles["sidebar-body"]} onClick={onClick}>
{children}
</div>
);
}
export function SideBarTail(props: {
primaryAction?: React.ReactNode;
secondaryAction?: React.ReactNode;
}) {
const { primaryAction, secondaryAction } = props;
return (
<div className={styles["sidebar-tail"]}>
<div className={styles["sidebar-actions"]}>{primaryAction}</div>
<div className={styles["sidebar-actions"]}>{secondaryAction}</div>
</div>
);
}
export function SideBar(props: { className?: string }) {
useHotKey();
const { onDragStart, shouldNarrow } = useDragSideBar();
const [showPluginSelector, setShowPluginSelector] = useState(false);
const navigate = useNavigate();
const config = useAppConfig();
const chatStore = useChatStore();
return (
<SideBarContainer
onDragStart={onDragStart}
shouldNarrow={shouldNarrow}
{...props}
>
<SideBarHeader
title="NextChat"
subTitle="Build your own AI assistant."
logo={<ChatGptIcon />}
shouldNarrow={shouldNarrow}
>
<div className={styles["sidebar-header-bar"]}>
<IconButton
icon={<MaskIcon />}
text={shouldNarrow ? undefined : Locale.Mask.Name}
className={styles["sidebar-bar-button"]}
onClick={() => {
if (config.dontShowMaskSplashScreen !== true) {
navigate(Path.NewChat, { state: { fromHome: true } });
} else {
navigate(Path.Masks, { state: { fromHome: true } });
}
}}
shadow
/>
<IconButton
icon={<DiscoveryIcon />}
text={shouldNarrow ? undefined : Locale.Discovery.Name}
className={styles["sidebar-bar-button"]}
onClick={() => setShowPluginSelector(true)}
shadow
/>
</div>
{showPluginSelector && (
<Selector
items={[
...PLUGINS.map((item) => {
return {
title: item.name,
value: item.path,
};
}),
]}
onClose={() => setShowPluginSelector(false)}
onSelection={(s) => {
navigate(s[0], { state: { fromHome: true } });
}}
/>
)}
</SideBarHeader>
<SideBarBody
onClick={(e) => { onClick={(e) => {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
navigate(Path.Home); navigate(Path.Home);
@ -197,32 +282,41 @@ export function SideBar(props: { className?: string }) {
}} }}
> >
<ChatList narrow={shouldNarrow} /> <ChatList narrow={shouldNarrow} />
</div> </SideBarBody>
<SideBarTail
<div className={styles["sidebar-tail"]}> primaryAction={
<div className={styles["sidebar-actions"]}> <>
<div className={styles["sidebar-action"] + " " + styles.mobile}> <div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton <IconButton
icon={<DeleteIcon />} icon={<DeleteIcon />}
onClick={async () => { onClick={async () => {
if (await showConfirm(Locale.Home.DeleteChat)) { if (await showConfirm(Locale.Home.DeleteChat)) {
chatStore.deleteSession(chatStore.currentSessionIndex); chatStore.deleteSession(chatStore.currentSessionIndex);
} }
}} }}
/> />
</div> </div>
<div className={styles["sidebar-action"]}> <div className={styles["sidebar-action"]}>
<Link to={Path.Settings}> <Link to={Path.Settings}>
<IconButton icon={<SettingsIcon />} shadow /> <IconButton
</Link> aria={Locale.Settings.Title}
</div> icon={<SettingsIcon />}
<div className={styles["sidebar-action"]}> shadow
<a href={REPO_URL} target="_blank" rel="noopener noreferrer"> />
<IconButton icon={<GithubIcon />} shadow /> </Link>
</a> </div>
</div> <div className={styles["sidebar-action"]}>
</div> <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
<div> <IconButton
aria={Locale.Export.MessageFromChatGPT}
icon={<GithubIcon />}
shadow
/>
</a>
</div>
</>
}
secondaryAction={
<IconButton <IconButton
icon={<AddIcon />} icon={<AddIcon />}
text={shouldNarrow ? undefined : Locale.Home.NewChat} text={shouldNarrow ? undefined : Locale.Home.NewChat}
@ -236,15 +330,8 @@ export function SideBar(props: { className?: string }) {
}} }}
shadow shadow
/> />
</div> }
</div> />
</SideBarContainer>
<div
className={styles["sidebar-drag"]}
onPointerDown={(e) => onDragStart(e as any)}
>
<DragIcon />
</div>
</div>
); );
} }

View File

@ -0,0 +1,133 @@
import { TTSConfig, TTSConfigValidator } from "../store";
import Locale from "../locales";
import { ListItem, Select } from "./ui-lib";
import {
DEFAULT_TTS_ENGINE,
DEFAULT_TTS_ENGINES,
DEFAULT_TTS_MODELS,
DEFAULT_TTS_VOICES,
} from "../constant";
import { InputRange } from "./input-range";
export function TTSConfigList(props: {
ttsConfig: TTSConfig;
updateConfig: (updater: (config: TTSConfig) => void) => void;
}) {
return (
<>
<ListItem
title={Locale.Settings.TTS.Enable.Title}
subTitle={Locale.Settings.TTS.Enable.SubTitle}
>
<input
type="checkbox"
checked={props.ttsConfig.enable}
onChange={(e) =>
props.updateConfig(
(config) => (config.enable = e.currentTarget.checked),
)
}
></input>
</ListItem>
{/* <ListItem
title={Locale.Settings.TTS.Autoplay.Title}
subTitle={Locale.Settings.TTS.Autoplay.SubTitle}
>
<input
type="checkbox"
checked={props.ttsConfig.autoplay}
onChange={(e) =>
props.updateConfig(
(config) => (config.autoplay = e.currentTarget.checked),
)
}
></input>
</ListItem> */}
<ListItem title={Locale.Settings.TTS.Engine}>
<Select
value={props.ttsConfig.engine}
onChange={(e) => {
props.updateConfig(
(config) =>
(config.engine = TTSConfigValidator.engine(
e.currentTarget.value,
)),
);
}}
>
{DEFAULT_TTS_ENGINES.map((v, i) => (
<option value={v} key={i}>
{v}
</option>
))}
</Select>
</ListItem>
{props.ttsConfig.engine === DEFAULT_TTS_ENGINE && (
<>
<ListItem title={Locale.Settings.TTS.Model}>
<Select
value={props.ttsConfig.model}
onChange={(e) => {
props.updateConfig(
(config) =>
(config.model = TTSConfigValidator.model(
e.currentTarget.value,
)),
);
}}
>
{DEFAULT_TTS_MODELS.map((v, i) => (
<option value={v} key={i}>
{v}
</option>
))}
</Select>
</ListItem>
<ListItem
title={Locale.Settings.TTS.Voice.Title}
subTitle={Locale.Settings.TTS.Voice.SubTitle}
>
<Select
value={props.ttsConfig.voice}
onChange={(e) => {
props.updateConfig(
(config) =>
(config.voice = TTSConfigValidator.voice(
e.currentTarget.value,
)),
);
}}
>
{DEFAULT_TTS_VOICES.map((v, i) => (
<option value={v} key={i}>
{v}
</option>
))}
</Select>
</ListItem>
<ListItem
title={Locale.Settings.TTS.Speed.Title}
subTitle={Locale.Settings.TTS.Speed.SubTitle}
>
<InputRange
aria={Locale.Settings.TTS.Speed.Title}
value={props.ttsConfig.speed?.toFixed(1)}
min="0.3"
max="4.0"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.speed = TTSConfigValidator.speed(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
</>
)}
</>
);
}

View File

@ -0,0 +1,119 @@
@import "../styles/animation.scss";
.plugin-page {
height: 100%;
display: flex;
flex-direction: column;
.plugin-page-body {
padding: 20px;
overflow-y: auto;
.plugin-filter {
width: 100%;
max-width: 100%;
margin-bottom: 20px;
animation: slide-in ease 0.3s;
height: 40px;
display: flex;
.search-bar {
flex-grow: 1;
max-width: 100%;
min-width: 0;
outline: none;
}
.search-bar:focus {
border: 1px solid var(--primary);
}
.plugin-filter-lang {
height: 100%;
margin-left: 10px;
}
.plugin-create {
height: 100%;
margin-left: 10px;
box-sizing: border-box;
min-width: 80px;
}
}
.plugin-item {
display: flex;
justify-content: space-between;
padding: 20px;
border: var(--border-in-light);
animation: slide-in ease 0.3s;
&:not(:last-child) {
border-bottom: 0;
}
&:first-child {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
&:last-child {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.plugin-header {
display: flex;
align-items: center;
.plugin-icon {
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.plugin-title {
.plugin-name {
font-size: 14px;
font-weight: bold;
}
.plugin-info {
font-size: 12px;
}
.plugin-runtime-warning {
font-size: 12px;
color: #f86c6c;
}
}
}
.plugin-actions {
display: flex;
flex-wrap: nowrap;
transition: all ease 0.3s;
justify-content: center;
align-items: center;
}
@media screen and (max-width: 600px) {
display: flex;
flex-direction: column;
padding-bottom: 10px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: var(--card-shadow);
&:not(:last-child) {
border-bottom: var(--border-in-light);
}
.plugin-actions {
width: 100%;
justify-content: space-between;
padding-top: 10px;
}
}
}
}
}

View File

@ -61,6 +61,19 @@
font-weight: normal; font-weight: normal;
} }
} }
&.vertical {
flex-direction: column;
align-items: start;
.list-header {
.list-item-title {
margin-bottom: 5px;
}
.list-item-sub-title {
margin-bottom: 2px;
}
}
}
} }
.list { .list {
@ -239,6 +252,12 @@
position: relative; position: relative;
max-width: fit-content; max-width: fit-content;
&.left-align-option {
option {
text-align: left;
}
}
.select-with-icon-select { .select-with-icon-select {
height: 100%; height: 100%;
border: var(--border-in-light); border: var(--border-in-light);
@ -291,7 +310,12 @@
justify-content: center; justify-content: center;
z-index: 999; z-index: 999;
.selector-item-disabled {
opacity: 0.6;
}
&-content { &-content {
min-width: 300px;
.list { .list {
max-height: 90vh; max-height: 90vh;
overflow-x: hidden; overflow-x: hidden;
@ -312,3 +336,4 @@
} }
} }
} }

View File

@ -13,7 +13,15 @@ import MinIcon from "../icons/min.svg";
import Locale from "../locales"; import Locale from "../locales";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import React, { HTMLProps, useEffect, useState } from "react"; import React, {
CSSProperties,
HTMLProps,
MouseEvent,
useEffect,
useState,
useCallback,
useRef,
} from "react";
import { IconButton } from "./button"; import { IconButton } from "./button";
export function Popover(props: { export function Popover(props: {
@ -42,16 +50,21 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
} }
export function ListItem(props: { export function ListItem(props: {
title: string; title?: string;
subTitle?: string; subTitle?: string | JSX.Element;
children?: JSX.Element | JSX.Element[]; children?: JSX.Element | JSX.Element[];
icon?: JSX.Element; icon?: JSX.Element;
className?: string; className?: string;
onClick?: () => void; onClick?: (e: MouseEvent) => void;
vertical?: boolean;
}) { }) {
return ( return (
<div <div
className={styles["list-item"] + ` ${props.className || ""}`} className={
styles["list-item"] +
` ${props.vertical ? styles["vertical"] : ""} ` +
` ${props.className || ""}`
}
onClick={props.onClick} onClick={props.onClick}
> >
<div className={styles["list-header"]}> <div className={styles["list-header"]}>
@ -252,9 +265,10 @@ export function Input(props: InputProps) {
); );
} }
export function PasswordInput(props: HTMLProps<HTMLInputElement>) { export function PasswordInput(
props: HTMLProps<HTMLInputElement> & { aria?: string },
) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
function changeVisibility() { function changeVisibility() {
setVisible(!visible); setVisible(!visible);
} }
@ -262,6 +276,7 @@ export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
return ( return (
<div className={"password-input-container"}> <div className={"password-input-container"}>
<IconButton <IconButton
aria={props.aria}
icon={visible ? <EyeIcon /> : <EyeOffIcon />} icon={visible ? <EyeIcon /> : <EyeOffIcon />}
onClick={changeVisibility} onClick={changeVisibility}
className={"password-eye"} className={"password-eye"}
@ -277,13 +292,19 @@ export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
export function Select( export function Select(
props: React.DetailedHTMLProps< props: React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>, React.SelectHTMLAttributes<HTMLSelectElement> & {
align?: "left" | "center";
},
HTMLSelectElement HTMLSelectElement
>, >,
) { ) {
const { className, children, ...otherProps } = props; const { className, children, align, ...otherProps } = props;
return ( return (
<div className={`${styles["select-with-icon"]} ${className}`}> <div
className={`${styles["select-with-icon"]} ${
align === "left" ? styles["left-align-option"] : ""
} ${className}`}
>
<select className={styles["select-with-icon-select"]} {...otherProps}> <select className={styles["select-with-icon-select"]} {...otherProps}>
{children} {children}
</select> </select>
@ -420,17 +441,25 @@ export function showPrompt(content: any, value = "", rows = 3) {
}); });
} }
export function showImageModal(img: string) { export function showImageModal(
img: string,
defaultMax?: boolean,
style?: CSSProperties,
boxStyle?: CSSProperties,
) {
showModal({ showModal({
title: Locale.Export.Image.Modal, title: Locale.Export.Image.Modal,
defaultMax: defaultMax,
children: ( children: (
<div> <div style={{ display: "flex", justifyContent: "center", ...boxStyle }}>
<img <img
src={img} src={img}
alt="preview" alt="preview"
style={{ style={
maxWidth: "100%", style ?? {
}} maxWidth: "100%",
}
}
></img> ></img>
</div> </div>
), ),
@ -442,27 +471,56 @@ export function Selector<T>(props: {
title: string; title: string;
subTitle?: string; subTitle?: string;
value: T; value: T;
disable?: boolean;
}>; }>;
defaultSelectedValue?: T; defaultSelectedValue?: T[] | T;
onSelection?: (selection: T[]) => void; onSelection?: (selection: T[]) => void;
onClose?: () => void; onClose?: () => void;
multiple?: boolean; multiple?: boolean;
}) { }) {
const [selectedValues, setSelectedValues] = useState<T[]>(
Array.isArray(props.defaultSelectedValue)
? props.defaultSelectedValue
: props.defaultSelectedValue !== undefined
? [props.defaultSelectedValue]
: [],
);
const handleSelection = (e: MouseEvent, value: T) => {
if (props.multiple) {
e.stopPropagation();
const newSelectedValues = selectedValues.includes(value)
? selectedValues.filter((v) => v !== value)
: [...selectedValues, value];
setSelectedValues(newSelectedValues);
props.onSelection?.(newSelectedValues);
} else {
setSelectedValues([value]);
props.onSelection?.([value]);
props.onClose?.();
}
};
return ( return (
<div className={styles["selector"]} onClick={() => props.onClose?.()}> <div className={styles["selector"]} onClick={() => props.onClose?.()}>
<div className={styles["selector-content"]}> <div className={styles["selector-content"]}>
<List> <List>
{props.items.map((item, i) => { {props.items.map((item, i) => {
const selected = props.defaultSelectedValue === item.value; const selected = selectedValues.includes(item.value);
return ( return (
<ListItem <ListItem
className={styles["selector-item"]} className={`${styles["selector-item"]} ${
item.disable && styles["selector-item-disabled"]
}`}
key={i} key={i}
title={item.title} title={item.title}
subTitle={item.subTitle} subTitle={item.subTitle}
onClick={() => { onClick={(e) => {
props.onSelection?.([item.value]); if (item.disable) {
props.onClose?.(); e.stopPropagation();
} else {
handleSelection(e, item.value);
}
}} }}
> >
{selected ? ( {selected ? (
@ -485,3 +543,38 @@ export function Selector<T>(props: {
</div> </div>
); );
} }
export function FullScreen(props: any) {
const { children, right = 10, top = 10, ...rest } = props;
const ref = useRef<HTMLDivElement>();
const [fullScreen, setFullScreen] = useState(false);
const toggleFullscreen = useCallback(() => {
if (!document.fullscreenElement) {
ref.current?.requestFullscreen();
} else {
document.exitFullscreen();
}
}, []);
useEffect(() => {
const handleScreenChange = (e: any) => {
if (e.target === ref.current) {
setFullScreen(!!document.fullscreenElement);
}
};
document.addEventListener("fullscreenchange", handleScreenChange);
return () => {
document.removeEventListener("fullscreenchange", handleScreenChange);
};
}, []);
return (
<div ref={ref} style={{ position: "relative" }} {...rest}>
<div style={{ position: "absolute", right, top }}>
<IconButton
icon={fullScreen ? <MinIcon /> : <MaxIcon />}
onClick={toggleFullscreen}
bordered
/>
</div>
{children}
</div>
);
}

View File

@ -3,7 +3,7 @@ import { BuildConfig, getBuildConfig } from "./build";
export function getClientConfig() { export function getClientConfig() {
if (typeof document !== "undefined") { if (typeof document !== "undefined") {
// client side // client side
return JSON.parse(queryMeta("config")) as BuildConfig; return JSON.parse(queryMeta("config") || "{}") as BuildConfig;
} }
if (typeof process !== "undefined") { if (typeof process !== "undefined") {

View File

@ -1,5 +1,5 @@
import md5 from "spark-md5"; import md5 from "spark-md5";
import { DEFAULT_MODELS } from "../constant"; import { DEFAULT_MODELS, DEFAULT_GA_ID } from "../constant";
declare global { declare global {
namespace NodeJS { namespace NodeJS {
@ -21,7 +21,11 @@ declare global {
ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
DISABLE_FAST_LINK?: string; // disallow parse settings from url or not DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
CUSTOM_MODELS?: string; // to control custom models CUSTOM_MODELS?: string; // to control custom models
DEFAULT_MODEL?: string; // to cnntrol default model in every new chat window DEFAULT_MODEL?: string; // to control default model in every new chat window
// stability only
STABILITY_URL?: string;
STABILITY_API_KEY?: string;
// azure only // azure only
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name} AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
@ -53,6 +57,24 @@ declare global {
ALIBABA_URL?: string; ALIBABA_URL?: string;
ALIBABA_API_KEY?: string; ALIBABA_API_KEY?: string;
// tencent only
TENCENT_URL?: string;
TENCENT_SECRET_KEY?: string;
TENCENT_SECRET_ID?: string;
// moonshot only
MOONSHOT_URL?: string;
MOONSHOT_API_KEY?: string;
// iflytek only
IFLYTEK_URL?: string;
IFLYTEK_API_KEY?: string;
IFLYTEK_API_SECRET?: string;
// xai only
XAI_URL?: string;
XAI_API_KEY?: string;
// custom template for preprocessing user input // custom template for preprocessing user input
DEFAULT_INPUT_TEMPLATE?: string; DEFAULT_INPUT_TEMPLATE?: string;
} }
@ -101,19 +123,34 @@ export const getServerSideConfig = () => {
if (disableGPT4) { if (disableGPT4) {
if (customModels) customModels += ","; if (customModels) customModels += ",";
customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4")) customModels += DEFAULT_MODELS.filter(
(m) =>
(m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o")) &&
!m.name.startsWith("gpt-4o-mini"),
)
.map((m) => "-" + m.name) .map((m) => "-" + m.name)
.join(","); .join(",");
if (defaultModel.startsWith("gpt-4")) defaultModel = ""; if (
(defaultModel.startsWith("gpt-4") ||
defaultModel.startsWith("chatgpt-4o")) &&
!defaultModel.startsWith("gpt-4o-mini")
)
defaultModel = "";
} }
const isStability = !!process.env.STABILITY_API_KEY;
const isAzure = !!process.env.AZURE_URL; const isAzure = !!process.env.AZURE_URL;
const isGoogle = !!process.env.GOOGLE_API_KEY; const isGoogle = !!process.env.GOOGLE_API_KEY;
const isAnthropic = !!process.env.ANTHROPIC_API_KEY; const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
const isTencent = !!process.env.TENCENT_API_KEY;
const isBaidu = !!process.env.BAIDU_API_KEY; const isBaidu = !!process.env.BAIDU_API_KEY;
const isBytedance = !!process.env.BYTEDANCE_API_KEY; const isBytedance = !!process.env.BYTEDANCE_API_KEY;
const isAlibaba = !!process.env.ALIBABA_API_KEY; const isAlibaba = !!process.env.ALIBABA_API_KEY;
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
const isIflytek = !!process.env.IFLYTEK_API_KEY;
const isXAI = !!process.env.XAI_API_KEY;
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
// const randomIndex = Math.floor(Math.random() * apiKeys.length); // const randomIndex = Math.floor(Math.random() * apiKeys.length);
@ -122,8 +159,8 @@ export const getServerSideConfig = () => {
// `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`, // `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
// ); // );
const allowedWebDevEndpoints = ( const allowedWebDavEndpoints = (
process.env.WHITE_WEBDEV_ENDPOINTS ?? "" process.env.WHITE_WEBDAV_ENDPOINTS ?? ""
).split(","); ).split(",");
return { return {
@ -131,6 +168,10 @@ export const getServerSideConfig = () => {
apiKey: getApiKey(process.env.OPENAI_API_KEY), apiKey: getApiKey(process.env.OPENAI_API_KEY),
openaiOrgId: process.env.OPENAI_ORG_ID, openaiOrgId: process.env.OPENAI_ORG_ID,
isStability,
stabilityUrl: process.env.STABILITY_URL,
stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
isAzure, isAzure,
azureUrl: process.env.AZURE_URL, azureUrl: process.env.AZURE_URL,
azureApiKey: getApiKey(process.env.AZURE_API_KEY), azureApiKey: getApiKey(process.env.AZURE_API_KEY),
@ -158,7 +199,31 @@ export const getServerSideConfig = () => {
alibabaUrl: process.env.ALIBABA_URL, alibabaUrl: process.env.ALIBABA_URL,
alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY), alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY),
isTencent,
tencentUrl: process.env.TENCENT_URL,
tencentSecretKey: getApiKey(process.env.TENCENT_SECRET_KEY),
tencentSecretId: process.env.TENCENT_SECRET_ID,
isMoonshot,
moonshotUrl: process.env.MOONSHOT_URL,
moonshotApiKey: getApiKey(process.env.MOONSHOT_API_KEY),
isIflytek,
iflytekUrl: process.env.IFLYTEK_URL,
iflytekApiKey: process.env.IFLYTEK_API_KEY,
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
isXAI,
xaiUrl: process.env.XAI_URL,
xaiApiKey: getApiKey(process.env.XAI_API_KEY),
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
cloudflareKVTTL: process.env.CLOUDFLARE_KV_TTL,
gtmId: process.env.GTM_ID, gtmId: process.env.GTM_ID,
gaId: process.env.GA_ID || DEFAULT_GA_ID,
needCode: ACCESS_CODES.size > 0, needCode: ACCESS_CODES.size > 0,
code: process.env.CODE, code: process.env.CODE,
@ -173,6 +238,6 @@ export const getServerSideConfig = () => {
disableFastLink: !!process.env.DISABLE_FAST_LINK, disableFastLink: !!process.env.DISABLE_FAST_LINK,
customModels, customModels,
defaultModel, defaultModel,
allowedWebDevEndpoints, allowedWebDavEndpoints,
}; };
}; };

View File

@ -1,6 +1,7 @@
export const OWNER = "Yidadaa"; export const OWNER = "ChatGPTNextWeb";
export const REPO = "ChatGPT-Next-Web"; export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
export const PLUGINS_REPO_URL = `https://github.com/${OWNER}/NextChat-Awesome-Plugins`;
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
export const UPDATE_URL = `${REPO_URL}#keep-updated`; export const UPDATE_URL = `${REPO_URL}#keep-updated`;
export const RELEASE_URL = `${REPO_URL}/releases`; export const RELEASE_URL = `${REPO_URL}/releases`;
@ -8,7 +9,8 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
export const DEFAULT_API_HOST = "https://api.nextchat.dev"; export const STABILITY_BASE_URL = "https://api.stability.ai";
export const OPENAI_BASE_URL = "https://api.openai.com"; export const OPENAI_BASE_URL = "https://api.openai.com";
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com"; export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
@ -21,13 +23,28 @@ export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/"; export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
export const XAI_BASE_URL = "https://api.x.ai";
export const CACHE_URL_PREFIX = "/api/cache";
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
export enum Path { export enum Path {
Home = "/", Home = "/",
Chat = "/chat", Chat = "/chat",
Settings = "/settings", Settings = "/settings",
NewChat = "/new-chat", NewChat = "/new-chat",
Masks = "/masks", Masks = "/masks",
Plugins = "/plugins",
Auth = "/auth", Auth = "/auth",
Sd = "/sd",
SdNew = "/sd-new",
Artifacts = "/artifacts",
SearchChat = "/search-chat",
} }
export enum ApiPath { export enum ApiPath {
@ -39,6 +56,12 @@ export enum ApiPath {
Baidu = "/api/baidu", Baidu = "/api/baidu",
ByteDance = "/api/bytedance", ByteDance = "/api/bytedance",
Alibaba = "/api/alibaba", Alibaba = "/api/alibaba",
Tencent = "/api/tencent",
Moonshot = "/api/moonshot",
Iflytek = "/api/iflytek",
Stability = "/api/stability",
Artifacts = "/api/artifacts",
XAI = "/api/xai",
} }
export enum SlotID { export enum SlotID {
@ -53,12 +76,14 @@ export enum FileName {
export enum StoreKey { export enum StoreKey {
Chat = "chat-next-web-store", Chat = "chat-next-web-store",
Plugin = "chat-next-web-plugin",
Access = "access-control", Access = "access-control",
Config = "app-config", Config = "app-config",
Mask = "mask-store", Mask = "mask-store",
Prompt = "prompt-store", Prompt = "prompt-store",
Update = "chat-update", Update = "chat-update",
Sync = "sync", Sync = "sync",
SdList = "sd-list",
} }
export const DEFAULT_SIDEBAR_WIDTH = 300; export const DEFAULT_SIDEBAR_WIDTH = 300;
@ -85,17 +110,41 @@ export enum ServiceProvider {
Baidu = "Baidu", Baidu = "Baidu",
ByteDance = "ByteDance", ByteDance = "ByteDance",
Alibaba = "Alibaba", Alibaba = "Alibaba",
Tencent = "Tencent",
Moonshot = "Moonshot",
Stability = "Stability",
Iflytek = "Iflytek",
XAI = "XAI",
}
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
// BLOCK_NONE will not block any content, and BLOCK_ONLY_HIGH will block only high-risk content.
export enum GoogleSafetySettingsThreshold {
BLOCK_NONE = "BLOCK_NONE",
BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH",
BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE",
BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE",
} }
export enum ModelProvider { export enum ModelProvider {
Stability = "Stability",
GPT = "GPT", GPT = "GPT",
GeminiPro = "GeminiPro", GeminiPro = "GeminiPro",
Claude = "Claude", Claude = "Claude",
Ernie = "Ernie", Ernie = "Ernie",
Doubao = "Doubao", Doubao = "Doubao",
Qwen = "Qwen", Qwen = "Qwen",
Hunyuan = "Hunyuan",
Moonshot = "Moonshot",
Iflytek = "Iflytek",
XAI = "XAI",
} }
export const Stability = {
GeneratePath: "v2beta/stable-image/generate",
ExampleEndpoint: "https://api.stability.ai",
};
export const Anthropic = { export const Anthropic = {
ChatPath: "v1/messages", ChatPath: "v1/messages",
ChatPath1: "v1/complete", ChatPath1: "v1/complete",
@ -105,6 +154,8 @@ export const Anthropic = {
export const OpenaiPath = { export const OpenaiPath = {
ChatPath: "v1/chat/completions", ChatPath: "v1/chat/completions",
SpeechPath: "v1/audio/speech",
ImagePath: "v1/images/generations",
UsagePath: "dashboard/billing/usage", UsagePath: "dashboard/billing/usage",
SubsPath: "dashboard/billing/subscription", SubsPath: "dashboard/billing/subscription",
ListModelPath: "v1/models", ListModelPath: "v1/models",
@ -113,12 +164,16 @@ export const OpenaiPath = {
export const Azure = { export const Azure = {
ChatPath: (deployName: string, apiVersion: string) => ChatPath: (deployName: string, apiVersion: string) =>
`deployments/${deployName}/chat/completions?api-version=${apiVersion}`, `deployments/${deployName}/chat/completions?api-version=${apiVersion}`,
ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}", // https://<your_resource_name>.openai.azure.com/openai/deployments/<your_deployment_name>/images/generations?api-version=<api_version>
ImagePath: (deployName: string, apiVersion: string) =>
`deployments/${deployName}/images/generations?api-version=${apiVersion}`,
ExampleEndpoint: "https://{resource-url}/openai",
}; };
export const Google = { export const Google = {
ExampleEndpoint: "https://generativelanguage.googleapis.com/", ExampleEndpoint: "https://generativelanguage.googleapis.com/",
ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`, ChatPath: (modelName: string) =>
`v1beta/models/${modelName}:streamGenerateContent`,
}; };
export const Baidu = { export const Baidu = {
@ -134,6 +189,9 @@ export const Baidu = {
if (modelName === "ernie-3.5-8k") { if (modelName === "ernie-3.5-8k") {
endpoint = "completions"; endpoint = "completions";
} }
if (modelName === "ernie-speed-8k") {
endpoint = "ernie_speed";
}
return `rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`; return `rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`;
}, },
}; };
@ -148,6 +206,25 @@ export const Alibaba = {
ChatPath: "v1/services/aigc/text-generation/generation", ChatPath: "v1/services/aigc/text-generation/generation",
}; };
export const Tencent = {
ExampleEndpoint: TENCENT_BASE_URL,
};
export const Moonshot = {
ExampleEndpoint: MOONSHOT_BASE_URL,
ChatPath: "v1/chat/completions",
};
export const Iflytek = {
ExampleEndpoint: IFLYTEK_BASE_URL,
ChatPath: "v1/chat/completions",
};
export const XAI = {
ExampleEndpoint: XAI_BASE_URL,
ChatPath: "v1/chat/completions",
};
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
// export const DEFAULT_SYSTEM_TEMPLATE = ` // export const DEFAULT_SYSTEM_TEMPLATE = `
// You are ChatGPT, a large language model trained by {{ServiceProvider}}. // You are ChatGPT, a large language model trained by {{ServiceProvider}}.
@ -166,7 +243,7 @@ Latex inline: \\(x^2\\)
Latex block: $$e=mc^2$$ Latex block: $$e=mc^2$$
`; `;
export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; export const SUMMARIZE_MODEL = "gpt-4o-mini";
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro"; export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
export const KnowledgeCutOffDate: Record<string, string> = { export const KnowledgeCutOffDate: Record<string, string> = {
@ -176,13 +253,33 @@ export const KnowledgeCutOffDate: Record<string, string> = {
"gpt-4-turbo-preview": "2023-12", "gpt-4-turbo-preview": "2023-12",
"gpt-4o": "2023-10", "gpt-4o": "2023-10",
"gpt-4o-2024-05-13": "2023-10", "gpt-4o-2024-05-13": "2023-10",
"gpt-4o-2024-08-06": "2023-10",
"chatgpt-4o-latest": "2023-10",
"gpt-4o-mini": "2023-10",
"gpt-4o-mini-2024-07-18": "2023-10",
"gpt-4-vision-preview": "2023-04", "gpt-4-vision-preview": "2023-04",
"o1-mini": "2023-10",
"o1-preview": "2023-10",
// After improvements, // After improvements,
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously. // it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
"gemini-pro": "2023-12", "gemini-pro": "2023-12",
"gemini-pro-vision": "2023-12", "gemini-pro-vision": "2023-12",
}; };
export const DEFAULT_TTS_ENGINE = "OpenAI-TTS";
export const DEFAULT_TTS_ENGINES = ["OpenAI-TTS", "Edge-TTS"];
export const DEFAULT_TTS_MODEL = "tts-1";
export const DEFAULT_TTS_VOICE = "alloy";
export const DEFAULT_TTS_MODELS = ["tts-1", "tts-1-hd"];
export const DEFAULT_TTS_VOICES = [
"alloy",
"echo",
"fable",
"onyx",
"nova",
"shimmer",
];
const openaiModels = [ const openaiModels = [
"gpt-3.5-turbo", "gpt-3.5-turbo",
"gpt-3.5-turbo-1106", "gpt-3.5-turbo-1106",
@ -195,9 +292,16 @@ const openaiModels = [
"gpt-4-turbo-preview", "gpt-4-turbo-preview",
"gpt-4o", "gpt-4o",
"gpt-4o-2024-05-13", "gpt-4o-2024-05-13",
"gpt-4o-2024-08-06",
"chatgpt-4o-latest",
"gpt-4o-mini",
"gpt-4o-mini-2024-07-18",
"gpt-4-vision-preview", "gpt-4-vision-preview",
"gpt-4-turbo-2024-04-09", "gpt-4-turbo-2024-04-09",
"gpt-4-1106-preview", "gpt-4-1106-preview",
"dall-e-3",
"o1-mini",
"o1-preview",
]; ];
const googleModels = [ const googleModels = [
@ -225,6 +329,10 @@ const baiduModels = [
"ernie-4.0-8k-latest", "ernie-4.0-8k-latest",
"ernie-3.5-8k", "ernie-3.5-8k",
"ernie-3.5-8k-0205", "ernie-3.5-8k-0205",
"ernie-speed-128k",
"ernie-speed-8k",
"ernie-lite-8k",
"ernie-tiny-8k",
]; ];
const bytedanceModels = [ const bytedanceModels = [
@ -246,68 +354,149 @@ const alibabaModes = [
"qwen-max-longcontext", "qwen-max-longcontext",
]; ];
const tencentModels = [
"hunyuan-pro",
"hunyuan-standard",
"hunyuan-lite",
"hunyuan-role",
"hunyuan-functioncall",
"hunyuan-code",
"hunyuan-vision",
];
const moonshotModes = ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"];
const iflytekModels = [
"general",
"generalv3",
"pro-128k",
"generalv3.5",
"4.0Ultra",
];
const xAIModes = ["grok-beta"];
let seq = 1000; // 内置的模型序号生成器从1000开始
export const DEFAULT_MODELS = [ export const DEFAULT_MODELS = [
...openaiModels.map((name) => ({ ...openaiModels.map((name) => ({
name, name,
available: true, available: true,
sorted: seq++, // Global sequence sort(index)
provider: { provider: {
id: "openai", id: "openai",
providerName: "OpenAI", providerName: "OpenAI",
providerType: "openai", providerType: "openai",
sorted: 1, // 这里是固定的,确保顺序与之前内置的版本一致
}, },
})), })),
...openaiModels.map((name) => ({ ...openaiModels.map((name) => ({
name, name,
available: true, available: true,
sorted: seq++,
provider: { provider: {
id: "azure", id: "azure",
providerName: "Azure", providerName: "Azure",
providerType: "azure", providerType: "azure",
sorted: 2,
}, },
})), })),
...googleModels.map((name) => ({ ...googleModels.map((name) => ({
name, name,
available: true, available: true,
sorted: seq++,
provider: { provider: {
id: "google", id: "google",
providerName: "Google", providerName: "Google",
providerType: "google", providerType: "google",
sorted: 3,
}, },
})), })),
...anthropicModels.map((name) => ({ ...anthropicModels.map((name) => ({
name, name,
available: true, available: true,
sorted: seq++,
provider: { provider: {
id: "anthropic", id: "anthropic",
providerName: "Anthropic", providerName: "Anthropic",
providerType: "anthropic", providerType: "anthropic",
sorted: 4,
}, },
})), })),
...baiduModels.map((name) => ({ ...baiduModels.map((name) => ({
name, name,
available: true, available: true,
sorted: seq++,
provider: { provider: {
id: "baidu", id: "baidu",
providerName: "Baidu", providerName: "Baidu",
providerType: "baidu", providerType: "baidu",
sorted: 5,
}, },
})), })),
...bytedanceModels.map((name) => ({ ...bytedanceModels.map((name) => ({
name, name,
available: true, available: true,
sorted: seq++,
provider: { provider: {
id: "bytedance", id: "bytedance",
providerName: "ByteDance", providerName: "ByteDance",
providerType: "bytedance", providerType: "bytedance",
sorted: 6,
}, },
})), })),
...alibabaModes.map((name) => ({ ...alibabaModes.map((name) => ({
name, name,
available: true, available: true,
sorted: seq++,
provider: { provider: {
id: "alibaba", id: "alibaba",
providerName: "Alibaba", providerName: "Alibaba",
providerType: "alibaba", providerType: "alibaba",
sorted: 7,
},
})),
...tencentModels.map((name) => ({
name,
available: true,
sorted: seq++,
provider: {
id: "tencent",
providerName: "Tencent",
providerType: "tencent",
sorted: 8,
},
})),
...moonshotModes.map((name) => ({
name,
available: true,
sorted: seq++,
provider: {
id: "moonshot",
providerName: "Moonshot",
providerType: "moonshot",
sorted: 9,
},
})),
...iflytekModels.map((name) => ({
name,
available: true,
sorted: seq++,
provider: {
id: "iflytek",
providerName: "Iflytek",
providerType: "iflytek",
sorted: 10,
},
})),
...xAIModes.map((name) => ({
name,
available: true,
sorted: seq++,
provider: {
id: "xai",
providerName: "XAI",
providerType: "xai",
sorted: 11,
}, },
})), })),
] as const; ] as const;
@ -327,3 +516,13 @@ export const internalAllowedWebDavEndpoints = [
"https://webdav.yandex.com", "https://webdav.yandex.com",
"https://app.koofr.net/dav/Koofr", "https://app.koofr.net/dav/Koofr",
]; ];
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
export const PLUGINS = [
{ name: "Plugins", path: Path.Plugins },
{ name: "Stable Diffusion", path: Path.Sd },
{ name: "Search Chat", path: Path.SearchChat },
];
export const SAAS_CHAT_URL = "https://nextchat.dev/chat";
export const SAAS_CHAT_UTM_URL = "https://nextchat.dev/chat?utm=github";

15
app/global.d.ts vendored
View File

@ -21,10 +21,23 @@ declare interface Window {
writeBinaryFile(path: string, data: Uint8Array): Promise<void>; writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
writeTextFile(path: string, data: string): Promise<void>; writeTextFile(path: string, data: string): Promise<void>;
}; };
notification:{ notification: {
requestPermission(): Promise<Permission>; requestPermission(): Promise<Permission>;
isPermissionGranted(): Promise<boolean>; isPermissionGranted(): Promise<boolean>;
sendNotification(options: string | Options): void; sendNotification(options: string | Options): void;
}; };
updater: {
checkUpdate(): Promise<UpdateResult>;
installUpdate(): Promise<void>;
onUpdaterEvent(
handler: (status: UpdateStatusResult) => void,
): Promise<UnlistenFn>;
};
http: {
fetch<T>(
url: string,
options?: Record<string, unknown>,
): Promise<Response<T>>;
};
}; };
} }

1
app/icons/arrow.svg Normal file
View File

@ -0,0 +1 @@
<svg class="icon--SJP_d" width="16" height="16" fill="none" viewBox="0 0 16 16" style="min-width: 16px; min-height: 16px;"><g><path data-follow-fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M5.248 14.444a.625.625 0 0 1-.005-.884l5.068-5.12a.625.625 0 0 0 0-.88L5.243 2.44a.625.625 0 1 1 .889-.88l5.067 5.121c.723.73.723 1.907 0 2.638l-5.067 5.12a.625.625 0 0 1-.884.005Z" fill="currentColor"></path></g></svg>

After

Width:  |  Height:  |  Size: 426 B

7
app/icons/discovery.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24">
<g fill="none" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<circle cx="12" cy="12" r="9" />
<path
d="M11.307 9.739L15 9l-.739 3.693a2 2 0 0 1-1.568 1.569L9 15l.739-3.693a2 2 0 0 1 1.568-1.568" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 371 B

1
app/icons/fire.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12.832 21.801c3.126-.626 7.168-2.875 7.168-8.69c0-5.291-3.873-8.815-6.658-10.434c-.619-.36-1.342.113-1.342.828v1.828c0 1.442-.606 4.074-2.29 5.169c-.86.559-1.79-.278-1.894-1.298l-.086-.838c-.1-.974-1.092-1.565-1.87-.971C4.461 8.46 3 10.33 3 13.11C3 20.221 8.289 22 10.933 22q.232 0 .484-.015C10.111 21.874 8 21.064 8 18.444c0-2.05 1.495-3.435 2.631-4.11c.306-.18.663.055.663.41v.59c0 .45.175 1.155.59 1.637c.47.546 1.159-.026 1.214-.744c.018-.226.246-.37.442-.256c.641.375 1.46 1.175 1.46 2.473c0 2.048-1.129 2.99-2.168 3.357"/></svg>

After

Width:  |  Height:  |  Size: 648 B

4
app/icons/hd.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#333" class="bi bi-badge-hd" viewBox="0 0 16 16">
<path d="M7.396 11V5.001H6.209v2.44H3.687V5H2.5v6h1.187V8.43h2.522V11zM8.5 5.001V11h2.188c1.811 0 2.685-1.107 2.685-3.015 0-1.894-.86-2.984-2.684-2.984zm1.187.967h.843c1.112 0 1.622.686 1.622 2.04 0 1.353-.505 2.02-1.622 2.02h-.843z"/>
<path d="M14 3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

10
app/icons/history.svg Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.81836 6.72729V14H13.0911" stroke="#333" stroke-width="4" stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M4 24C4 35.0457 12.9543 44 24 44V44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C16.598 4 10.1351 8.02111 6.67677 13.9981"
stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
<path d="M24.005 12L24.0038 24.0088L32.4832 32.4882" stroke="#333" stroke-width="4"
stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 660 B

19
app/icons/logo.svg Normal file
View File

@ -0,0 +1,19 @@
<svg width="38.73" height="42" viewBox="0 0 221 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="160.697" y="38.125" width="65.007" height="145.932" rx="32.503" transform="rotate(21.987 160.697 38.125)" fill="url(#logo_svg__a)"></rect>
<path fill-rule="evenodd" clip-rule="evenodd" d="m48.642 79.125-25.92 71.213c-6.139 16.869 2.558 35.52 19.427 41.66 16.868 6.14 35.52-2.558 41.66-19.426L94.23 143.94l-36.658-37.439a32.42 32.42 0 0 1-9.244-23.497c.033-1.326.14-2.62.314-3.879Z" fill="url(#logo_svg__b)"></path>
<path d="M172.578 132.787a32.765 32.765 0 0 1 8.981 24.238c-1.458 28.748-36.622 41.778-56.46 20.92l-67.644-71.122a32.763 32.763 0 0 1-8.981-24.238c1.457-28.748 36.622-41.778 56.46-20.92l67.644 71.122Z" fill="url(#logo_svg__c)" fill-opacity="0.96"></path>
<defs>
<linearGradient id="logo_svg__a" x1="215.063" y1="59.628" x2="160.714" y2="157.96" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EADFE"></stop>
<stop offset="1" stop-color="#2A7AFF"></stop>
</linearGradient>
<linearGradient id="logo_svg__b" x1="105.376" y1="84.416" x2="19.745" y2="131.163" gradientUnits="userSpaceOnUse">
<stop stop-color="#01B3FF"></stop>
<stop offset="1" stop-color="#59ECFA"></stop>
</linearGradient>
<linearGradient id="logo_svg__c" x1="102.734" y1="136.396" x2="192.577" y2="155.859" gradientUnits="userSpaceOnUse">
<stop stop-color="#023BFF" stop-opacity="0.82"></stop>
<stop offset="0.88" stop-color="#2D86FF" stop-opacity="0.76"></stop>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

4
app/icons/palette.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#333" class="bi bi-palette" viewBox="0 0 16 16">
<path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3M5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/>
<path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8m-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7"/>
</svg>

After

Width:  |  Height:  |  Size: 781 B

12
app/icons/sd.svg Normal file
View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1.21em" height="1em" viewBox="0 0 256 213">
<defs>
<linearGradient id="logosStabilityAiIcon0" x1="50%" x2="50%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#9d39ff" />
<stop offset="100%" stop-color="#a380ff" />
</linearGradient>
</defs>
<path fill="url(#logosStabilityAiIcon0)"
d="M72.418 212.45c49.478 0 81.658-26.205 81.658-65.626c0-30.572-19.572-49.998-54.569-58.043l-22.469-6.74c-19.71-4.424-31.215-9.738-28.505-23.312c2.255-11.292 9.002-17.667 24.69-17.667c49.872 0 68.35 17.667 68.35 17.667V16.237S123.583 0 73.223 0C25.757 0 0 24.424 0 62.236c0 30.571 17.85 48.35 54.052 56.798q3.802.95 3.885.976q8.26 2.556 22.293 6.755c18.504 4.425 23.262 9.121 23.262 23.2c0 12.872-13.374 20.19-31.074 20.19C21.432 170.154 0 144.36 0 144.36v47.078s13.402 21.01 72.418 21.01" />
<path fill="#e80000"
d="M225.442 209.266c17.515 0 30.558-12.67 30.558-29.812c0-17.515-12.67-29.813-30.558-29.813c-17.515 0-30.185 12.298-30.185 29.813s12.67 29.812 30.185 29.812" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M42 7H6C4.89543 7 4 7.89543 4 9V37C4 38.1046 4.89543 39 6 39H42C43.1046 39 44 38.1046 44 37V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"/><path d="M12 19H14" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 19H23" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M29 19H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 28H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 734 B

1
app/icons/size.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M42 7H6C4.89543 7 4 7.89543 4 9V39C4 40.1046 4.89543 41 6 41H42C43.1046 41 44 40.1046 44 39V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#333" stroke-width="4"/><path d="M30 30V18L38 30V18" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 30V18L18 30V18" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M24 20V21" stroke="#333" stroke-width="4" stroke-linecap="round"/><path d="M24 27V28" stroke="#333" stroke-width="4" stroke-linecap="round"/></svg>

After

Width:  |  Height:  |  Size: 681 B

1
app/icons/speak-stop.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"></path></svg>

After

Width:  |  Height:  |  Size: 495 B

1
app/icons/speak.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"></path></svg>

After

Width:  |  Height:  |  Size: 485 B

16
app/icons/voice-white.svg Normal file
View File

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 20 20">
<defs>
<rect id="path_0" width="20" height="20" x="0" y="0" />
</defs>
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
<mask id="bg-mask-0" fill="#fff">
<use xlink:href="#path_0" />
</mask>
<g mask="url(#bg-mask-0)">
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" fill="#333333">
</path>
<path d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z" fill="#333333">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 708 B

1
app/icons/zoom.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24"><g fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></g></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@ -6,7 +6,7 @@ import { getClientConfig } from "./config/client";
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { SpeedInsights } from "@vercel/speed-insights/next"; import { SpeedInsights } from "@vercel/speed-insights/next";
import { getServerSideConfig } from "./config/server"; import { getServerSideConfig } from "./config/server";
import { GoogleTagManager } from "@next/third-parties/google"; import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google";
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
export const metadata: Metadata = { export const metadata: Metadata = {
@ -37,8 +37,15 @@ export default function RootLayout({
<html lang="en"> <html lang="en">
<head> <head>
<meta name="config" content={JSON.stringify(getClientConfig())} /> <meta name="config" content={JSON.stringify(getClientConfig())} />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta
<link rel="manifest" href="/site.webmanifest"></link> name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link
rel="manifest"
href="/site.webmanifest"
crossOrigin="use-credentials"
></link>
<script src="/serviceWorkerRegister.js" defer></script> <script src="/serviceWorkerRegister.js" defer></script>
</head> </head>
<body> <body>
@ -53,6 +60,11 @@ export default function RootLayout({
<GoogleTagManager gtmId={serverConfig.gtmId} /> <GoogleTagManager gtmId={serverConfig.gtmId} />
</> </>
)} )}
{serverConfig?.gaId && (
<>
<GoogleAnalytics gaId={serverConfig.gaId} />
</>
)}
</body> </body>
</html> </html>
); );

View File

@ -1,274 +1,558 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index"; import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
const ar: PartialLocaleType = { const ar: PartialLocaleType = {
WIP: "قريبًا...", WIP: "قريبًا...",
Error: { Error: {
Unauthorized: Unauthorized: isApp
"غير مصرح بالوصول، يرجى إدخال رمز الوصول [auth](/#/auth) في صفحة المصادقة.", ? `😆 واجهت المحادثة بعض المشكلات، لا داعي للقلق:
\\ 1 إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_UTM_URL})
\\ 2 إذا كنت تريد استخدام موارد OpenAI الخاصة بك، انقر [هنا](/#/settings) لتعديل الإعدادات `
: `😆 واجهت المحادثة بعض المشكلات، لا داعي للقلق:
\ 1 إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_UTM_URL})
\ 2 إذا كنت تستخدم إصدار النشر الخاص، انقر [هنا](/#/auth) لإدخال مفتاح الوصول 🔑
\ 3 إذا كنت تريد استخدام موارد OpenAI الخاصة بك، انقر [هنا](/#/settings) لتعديل الإعدادات
`,
}, },
Auth: { Auth: {
Title: "تحتاج إلى رمز الوصول", Title: "تحتاج إلى كلمة مرور",
Tips: "يرجى إدخال رمز الوصول أدناه", Tips: "قام المشرف بتفعيل التحقق بكلمة المرور، يرجى إدخال رمز الوصول أدناه",
SubTips: "أو أدخل مفتاح واجهة برمجة تطبيقات OpenAI الخاص بك", SubTips: "أو إدخال مفتاح API الخاص بـ OpenAI أو Google",
Input: "رمز الوصول", Input: "أدخل رمز الوصول هنا",
Confirm: "تأكيد", Confirm: "تأكيد",
Later: "لاحقًا", Later: "في وقت لاحق",
Return: "عودة",
SaasTips: "الإعدادات معقدة، أريد استخدامه على الفور",
TopTips:
"🥳 عرض NextChat AI الأول، افتح الآن OpenAI o1, GPT-4o, Claude-3.5 وأحدث النماذج الكبيرة",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} رسائل`, ChatItemCount: (count: number) => `${count} محادثة`,
}, },
Chat: { Chat: {
SubTitle: (count: number) => ` ${count} رسائل مع ChatGPT`, SubTitle: (count: number) => `إجمالي ${count} محادثة`,
EditMessage: {
Title: "تحرير سجل الرسائل",
Topic: {
Title: "موضوع الدردشة",
SubTitle: "تغيير موضوع الدردشة الحالي",
},
},
Actions: { Actions: {
ChatList: "الانتقال إلى قائمة الدردشة", ChatList: "عرض قائمة الرسائل",
CompressedHistory: "ملخص ضغط ذاكرة التاريخ", CompressedHistory: "عرض التاريخ المضغوط",
Export: "تصدير جميع الرسائل كـ Markdown", Export: "تصدير سجل الدردشة",
Copy: "نسخ", Copy: "نسخ",
Stop: "توقف", Stop: "إيقاف",
Retry: "إعادة المحاولة", Retry: "إعادة المحاولة",
Pin: "تثبيت",
PinToastContent: "تم تثبيت 1 محادثة في الإشعارات المسبقة",
PinToastAction: "عرض",
Delete: "حذف", Delete: "حذف",
Edit: "تحرير",
RefreshTitle: "تحديث العنوان",
RefreshToast: "تم إرسال طلب تحديث العنوان",
},
Commands: {
new: "دردشة جديدة",
newm: "إنشاء دردشة من القناع",
next: "الدردشة التالية",
prev: "الدردشة السابقة",
clear: "مسح السياق",
del: "حذف الدردشة",
}, },
InputActions: { InputActions: {
Stop: "توقف", Stop: "إيقاف الاستجابة",
ToBottom: "إلى آخر", ToBottom: "الانتقال إلى الأحدث",
Theme: { Theme: {
auto: "تلقائي", auto: "موضوع تلقائي",
light: "نمط فاتح", light: "الوضع الفاتح",
dark: "نمط داكن", dark: "الوضع الداكن",
}, },
Prompt: "الاقتراحات", Prompt: "الأوامر السريعة",
Masks: "الأقنعة", Masks: "جميع الأقنعة",
Clear: "مسح السياق", Clear: "مسح الدردشة",
Settings: "الإعدادات", Settings: "إعدادات الدردشة",
UploadImage: "تحميل صورة",
}, },
Rename: "إعادة تسمية الدردشة", Rename: "إعادة تسمية الدردشة",
Typing: "كتابة...", Typing: "يكتب…",
Input: (submitKey: string) => { Input: (submitKey: string) => {
var inputHints = ` اضغط على ${submitKey} للإرسال`; var inputHints = `${submitKey} إرسال`;
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += "، Shift + Enter للإنشاء"; inputHints += "، Shift + Enter لإدراج سطر جديد";
} }
return inputHints + "، / للبحث في الاقتراحات"; return inputHints + "، / لتفعيل الإكمال التلقائي، : لتفعيل الأوامر";
}, },
Send: "إرسال", Send: "إرسال",
Config: { Config: {
Reset: "إعادة التعيين إلى الإعدادات الافتراضية", Reset: "مسح الذاكرة",
SaveAs: "حفظ كأقنعة", SaveAs: "حفظ كقناع",
}, },
IsContext: "الإشعارات المسبقة",
}, },
Export: { Export: {
Title: "تصدير الرسائل", Title: "مشاركة سجل الدردشة",
Copy: "نسخ الكل", Copy: "نسخ الكل",
Download: "تنزيل", Download: "تحميل الملف",
MessageFromYou: "رسالة منك",
MessageFromChatGPT: "رسالة من ChatGPT",
Share: "مشاركة على ShareGPT", Share: "مشاركة على ShareGPT",
MessageFromYou: "المستخدم",
MessageFromChatGPT: "ChatGPT",
Format: { Format: {
Title: "صيغة التصدير", Title: "تنسيق التصدير",
SubTitle: "Markdown أو صورة PNG", SubTitle: "يمكنك تصدير النص كـ Markdown أو صورة PNG",
}, },
IncludeContext: { IncludeContext: {
Title: "تضمين السياق", Title: "تضمين سياق القناع",
SubTitle: "تصدير اقتراحات السياق في الأقنعة أم لا", SubTitle: "هل تريد عرض سياق القناع في الرسائل",
}, },
Steps: { Steps: {
Select: "تحديد", Select: "اختيار",
Preview: "معاينة", Preview: "معاينة",
}, },
Image: {
Toast: "يتم إنشاء لقطة الشاشة",
Modal: "اضغط مطولاً أو انقر بزر الماوس الأيمن لحفظ الصورة",
},
}, },
Select: { Select: {
Search: "بحث", Search: "بحث في الرسائل",
All: "تحديد الكل", All: "تحديد الكل",
Latest: "تحديد أحدث", Latest: "أحدث الرسائل",
Clear: "مسح", Clear: "مسح التحديد",
}, },
Memory: { Memory: {
Title: "اقتراحات الذاكرة", Title: "ملخص التاريخ",
EmptyContent: "لا شيء حتى الآن.", EmptyContent: "محتوى المحادثة قصير جداً، لا حاجة للتلخيص",
Send: "إرسال الذاكرة", Send: "ضغط تلقائي لسجل الدردشة كـ سياق",
Copy: "نسخ الذاكرة", Copy: "نسخ الملخص",
Reset: "إعادة التعيين", Reset: "[غير مستخدم]",
ResetConfirm: ResetConfirm: "تأكيد مسح ملخص التاريخ؟",
"سيؤدي إعادة التعيين إلى مسح سجل المحادثة الحالي والذاكرة التاريخية. هل أنت متأكد أنك تريد الاستمرار؟",
}, },
Home: { Home: {
NewChat: "دردشة جديدة", NewChat: "دردشة جديدة",
DeleteChat: "هل تريد تأكيد حذف المحادثة المحددة؟", DeleteChat: "تأكيد حذف المحادثة المحددة؟",
DeleteToast: "تم حذف الدردشة", DeleteToast: "تم حذف المحادثة",
Revert: "التراجع", Revert: "تراجع",
}, },
Settings: { Settings: {
Title: "الإعدادات", Title: "الإعدادات",
SubTitle: "جميع الإعدادات", SubTitle: "جميع خيارات الإعدادات",
Lang: { Danger: {
Name: "Language", // تنبيه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة وتركها "Language" Reset: {
All: "كل اللغات", Title: "إعادة تعيين جميع الإعدادات",
SubTitle: "إعادة تعيين جميع عناصر الإعدادات إلى القيم الافتراضية",
Action: "إعادة التعيين الآن",
Confirm: "تأكيد إعادة تعيين جميع الإعدادات؟",
},
Clear: {
Title: "مسح جميع البيانات",
SubTitle: "مسح جميع الدردشات وبيانات الإعدادات",
Action: "مسح الآن",
Confirm: "تأكيد مسح جميع الدردشات وبيانات الإعدادات؟",
},
}, },
Avatar: "الصورة الرمزية", Lang: {
Name: "Language", // انتبه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة، اتركها كما هي "Language"
All: "جميع اللغات",
},
Avatar: "الصورة الشخصية",
FontSize: { FontSize: {
Title: "حجم الخط", Title: "حجم الخط",
SubTitle: "ضبط حجم الخط لمحتوى الدردشة", SubTitle: "حجم الخط في محتوى الدردشة",
},
FontFamily: {
Title: "خط الدردشة",
SubTitle: "خط محتوى الدردشة، اتركه فارغًا لتطبيق الخط الافتراضي العالمي",
Placeholder: "اسم الخط",
}, },
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "حقن تلميحات النظام", Title: "حقن الرسائل النصية النظامية",
SubTitle: SubTitle:
"قم بإضافة تلميحة نظام محاكاة ChatGPT إلى بداية قائمة الرسائل المُطلَبة في كل طلب", "فرض إضافة رسالة نظامية تحاكي ChatGPT في بداية قائمة الرسائل لكل طلب",
}, },
InputTemplate: { InputTemplate: {
Title: "نموذج الإدخال", Title: "معالجة الإدخال من قبل المستخدم",
SubTitle: "سيتم ملء أحدث رسالة في هذا النموذج", SubTitle: "سيتم ملء آخر رسالة من المستخدم في هذا القالب",
}, },
Update: { Update: {
Version: (x: string) => ` الإصدار: ${x}`, Version: (x: string) => `الإصدار الحالي: ${x}`,
IsLatest: حدث إصدار", IsLatest: نت على أحدث إصدار",
CheckUpdate: "التحقق من التحديث", CheckUpdate: "التحقق من التحديثات",
IsChecking: "جارٍ التحقق من التحديث...", IsChecking: "جارٍ التحقق من التحديثات...",
FoundUpdate: (x: string) => ` تم العثور على إصدار جديد: ${x}`, FoundUpdate: (x: string) => `تم العثور على إصدار جديد: ${x}`,
GoToUpdate: "التحديث", GoToUpdate: "انتقل للتحديث",
}, },
SendKey: "مفتاح الإرسال", SendKey: "زر الإرسال",
Theme: "السمة", Theme: "السمة",
TightBorder: "حدود ضيقة", TightBorder: "وضع بدون حدود",
SendPreviewBubble: { SendPreviewBubble: {
Title: "عرض معاينة الـ Send", Title: "فقاعة المعاينة",
SubTitle: "معاينة Markdown في فقاعة", SubTitle: "معاينة محتوى Markdown في فقاعة المعاينة",
},
AutoGenerateTitle: {
Title: "توليد العنوان تلقائيًا",
SubTitle: "توليد عنوان مناسب بناءً على محتوى الدردشة",
},
Sync: {
CloudState: "بيانات السحابة",
NotSyncYet: "لم يتم التزامن بعد",
Success: "تم التزامن بنجاح",
Fail: "فشل التزامن",
Config: {
Modal: {
Title: "تكوين التزامن السحابي",
Check: "التحقق من التوفر",
},
SyncType: {
Title: "نوع التزامن",
SubTitle: "اختر خادم التزامن المفضل",
},
Proxy: {
Title: "تفعيل الوكيل",
SubTitle: "يجب تفعيل الوكيل عند التزامن عبر المتصفح لتجنب قيود CORS",
},
ProxyUrl: {
Title: "عنوان الوكيل",
SubTitle: "ينطبق فقط على الوكيل المتاح في هذا المشروع",
},
WebDav: {
Endpoint: "عنوان WebDAV",
UserName: "اسم المستخدم",
Password: "كلمة المرور",
},
UpStash: {
Endpoint: "رابط UpStash Redis REST",
UserName: "اسم النسخ الاحتياطي",
Password: "رمز UpStash Redis REST",
},
},
LocalState: "بيانات محلية",
Overview: (overview: any) => {
return `${overview.chat} دردشة، ${overview.message} رسالة، ${overview.prompt} إشعار، ${overview.mask} قناع`;
},
ImportFailed: "فشل الاستيراد",
}, },
Mask: { Mask: {
Splash: { Splash: {
Title: "شاشة تظهر الأقنعة", Title: "صفحة بدء القناع",
SubTitle: "عرض شاشة تظهر الأقنعة قبل بدء الدردشة الجديدة", SubTitle: "عرض صفحة بدء القناع عند بدء دردشة جديدة",
},
Builtin: {
Title: "إخفاء الأقنعة المدمجة",
SubTitle: "إخفاء الأقنعة المدمجة في قائمة الأقنعة",
}, },
}, },
Prompt: { Prompt: {
Disable: { Disable: {
Title: "تعطيل الاكتمال التلقائي", Title: "تعطيل الإكمال التلقائي للإشعارات",
SubTitle: "اكتب / لتشغيل الاكتمال التلقائي", SubTitle: "استخدم / في بداية مربع النص لتفعيل الإكمال التلقائي",
}, },
List: "قائمة الاقتراحات", List: "قائمة الإشعارات المخصصة",
ListCount: (builtin: number, custom: number) => ` ListCount: (builtin: number, custom: number) =>
${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخدم`, `مدمج ${builtin} إشعار، مخصص ${custom} إشعار`,
Edit: "تعديل", Edit: حرير",
Modal: { Modal: {
Title: "قائمة الاقتراحات", Title: "قائمة الإشعارات",
Add: "إضافة واحدة", Add: "جديد",
Search: "البحث في الاقتراحات", Search: "بحث عن إشعارات",
}, },
EditModal: { EditModal: {
Title: "تحرير الاقتراح", Title: "تحرير الإشعارات",
}, },
}, },
HistoryCount: { HistoryCount: {
Title: "عدد الرسائل المرفقة", Title: "عدد الرسائل التاريخية المرفقة",
SubTitle: "عدد الرسائل المرسلة المرفقة في كل طلب", SubTitle: "عدد الرسائل التاريخية المرفقة مع كل طلب",
}, },
CompressThreshold: { CompressThreshold: {
Title: "حد الضغط للتاريخ", Title: "عتبة ضغط طول الرسائل التاريخية",
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد", SubTitle:
"عندما يتجاوز طول الرسائل التاريخية غير المضغوطة هذه القيمة، سيتم الضغط",
}, },
Usage: { Usage: {
Title: "رصيد الحساب", Title: "التحقق من الرصيد",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
return `تم استخدام $${used} من هذا الشهر، الاشتراك ${total}`; return `تم استخدام $${used} هذا الشهر، إجمالي الاشتراك $${total}`;
}, },
IsChecking: "جارٍ التحقق...", IsChecking: "جارٍ التحقق...",
Check: "التحقق", Check: "إعادة التحقق",
NoAccess: "أدخل مفتاح API للتحقق من الرصيد", NoAccess: "أدخل مفتاح API أو كلمة مرور للوصول إلى الرصيد",
},
Access: {
SaasStart: {
Title: "استخدام NextChat AI",
Label: "(أفضل حل من حيث التكلفة)",
SubTitle:
"مدعوم رسميًا من NextChat، جاهز للاستخدام بدون إعداد، يدعم أحدث النماذج الكبيرة مثل OpenAI o1 و GPT-4o و Claude-3.5",
ChatNow: "الدردشة الآن",
},
AccessCode: {
Title: "كلمة المرور للوصول",
SubTitle: "قام المشرف بتمكين الوصول المشفر",
Placeholder: "أدخل كلمة المرور للوصول",
},
CustomEndpoint: {
Title: "واجهة مخصصة",
SubTitle: "هل تستخدم خدمة Azure أو OpenAI مخصصة",
},
Provider: {
Title: "موفر الخدمة النموذجية",
SubTitle: "التبديل بين مقدمي الخدمة المختلفين",
},
OpenAI: {
ApiKey: {
Title: "مفتاح API",
SubTitle: "استخدم مفتاح OpenAI مخصص لتجاوز قيود كلمة المرور",
Placeholder: "مفتاح OpenAI API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "يجب أن يحتوي على http(s):// بخلاف العنوان الافتراضي",
},
},
Azure: {
ApiKey: {
Title: "مفتاح الواجهة",
SubTitle: "استخدم مفتاح Azure مخصص لتجاوز قيود كلمة المرور",
Placeholder: "مفتاح Azure API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "مثال:",
},
ApiVerion: {
Title: "إصدار الواجهة (azure api version)",
SubTitle: "اختر إصدارًا معينًا",
},
},
Anthropic: {
ApiKey: {
Title: "مفتاح الواجهة",
SubTitle: "استخدم مفتاح Anthropic مخصص لتجاوز قيود كلمة المرور",
Placeholder: "مفتاح Anthropic API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "مثال:",
},
ApiVerion: {
Title: "إصدار الواجهة (claude api version)",
SubTitle: "اختر إصدار API محدد",
},
},
Google: {
ApiKey: {
Title: "مفتاح API",
SubTitle: "احصل على مفتاح API الخاص بك من Google AI",
Placeholder: "أدخل مفتاح Google AI Studio API",
},
Endpoint: {
Title: "عنوان النهاية",
SubTitle: "مثال:",
},
ApiVersion: {
Title: "إصدار API (مخصص لـ gemini-pro)",
SubTitle: "اختر إصدار API معين",
},
GoogleSafetySettings: {
Title: "مستوى تصفية الأمان من Google",
SubTitle: "تعيين مستوى تصفية المحتوى",
},
},
Baidu: {
ApiKey: {
Title: "مفتاح API",
SubTitle: "استخدم مفتاح Baidu API مخصص",
Placeholder: "مفتاح Baidu API",
},
SecretKey: {
Title: "المفتاح السري",
SubTitle: "استخدم مفتاح Baidu Secret مخصص",
Placeholder: "مفتاح Baidu Secret",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "لا يدعم التخصيص، انتقل إلى .env للتكوين",
},
},
ByteDance: {
ApiKey: {
Title: "مفتاح الواجهة",
SubTitle: "استخدم مفتاح ByteDance API مخصص",
Placeholder: "مفتاح ByteDance API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "مثال:",
},
},
Alibaba: {
ApiKey: {
Title: "مفتاح الواجهة",
SubTitle: "استخدم مفتاح Alibaba Cloud API مخصص",
Placeholder: "مفتاح Alibaba Cloud API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "مثال:",
},
},
CustomModel: {
Title: "اسم النموذج المخصص",
SubTitle: "أضف خيارات نموذج مخصص، مفصولة بفواصل إنجليزية",
},
}, },
Model: "النموذج", Model: "النموذج",
CompressModel: {
Title: "نموذج الضغط",
SubTitle: "النموذج المستخدم لضغط السجل التاريخي",
},
Temperature: { Temperature: {
Title: "الحرارة", Title: "العشوائية (temperature)",
SubTitle: "قيمة أكبر تجعل الإخراج أكثر عشوائية", SubTitle: "كلما زادت القيمة، زادت العشوائية في الردود",
},
TopP: {
Title: "عينات النواة (top_p)",
SubTitle: "مشابه للعشوائية ولكن لا تغيره مع العشوائية",
}, },
MaxTokens: { MaxTokens: {
Title: "الحد الأقصى للرموز", Title: "حد أقصى للرموز لكل رد (max_tokens)",
SubTitle: "الحد الأقصى لعدد الرموز المدخلة والرموز المُنشأة", SubTitle: "أقصى عدد للرموز في تفاعل واحد",
}, },
PresencePenalty: { PresencePenalty: {
Title: "تأثير الوجود", Title: جدد الموضوع (presence_penalty)",
SubTitle: "قيمة أكبر تزيد من احتمالية التحدث عن مواضيع جديدة", SubTitle: "كلما زادت القيمة، زادت احتمالية التوسع في مواضيع جديدة",
}, },
FrequencyPenalty: { FrequencyPenalty: {
Title: "تأثير التكرار", Title: "عقوبة التكرار (frequency_penalty)",
SubTitle: "قيمة أكبر تقلل من احتمالية تكرار نفس السطر", SubTitle: "كلما زادت القيمة، زادت احتمالية تقليل تكرار الكلمات",
}, },
}, },
Store: { Store: {
DefaultTopic: "محادثة جديدة", DefaultTopic: "دردشة جديدة",
BotHello: "مرحبًا! كيف يمكنني مساعدتك اليوم؟", BotHello: "كيف يمكنني مساعدتك؟",
Error: "حدث خطأ ما، يرجى المحاولة مرة أخرى في وقت لاحق.", Error: "حدث خطأ، يرجى المحاولة مرة أخرى لاحقًا",
Prompt: { Prompt: {
History: (content: string) => "هذا ملخص لسجل الدردشة كمراجعة: " + content, History: (content: string) =>
"هذا ملخص للدردشة السابقة كنقطة انطلاق: " + content,
Topic: Topic:
"يرجى إنشاء عنوان يتكون من أربع إلى خمس كلمات يلخص محادثتنا دون أي مقدمة أو ترقيم أو علامات ترقيم أو نقاط أو رموز إضافية. قم بإزالة علامات التنصيص المحيطة.", "استخدم أربع إلى خمس كلمات لإرجاع ملخص مختصر لهذه الجملة، بدون شرح، بدون علامات ترقيم، بدون كلمات تعبيرية، بدون نص إضافي، بدون تنسيق عريض، إذا لم يكن هناك موضوع، يرجى العودة إلى 'دردشة عامة'",
Summarize: Summarize:
"قم بتلخيص النقاش بشكل موجز في 200 كلمة أو أقل لاستخدامه كاقتراح للسياق في المستقبل.", "قم بتلخيص محتوى الدردشة باختصار، لاستخدامه كإشارة سياقية لاحقة، اجعلها في حدود 200 كلمة",
}, },
}, },
Copy: { Copy: {
Success: "تم النسخ إلى الحافظة", Success: "تم الكتابة إلى الحافظة",
Failed: "فشلت عملية النسخ، يرجى منح الإذن للوصول إلى الحافظة", Failed: "فشل النسخ، يرجى منح أذونات الحافظة",
},
Download: {
Success: "تم تنزيل المحتوى إلى مجلدك.",
Failed: "فشل التنزيل.",
}, },
Context: { Context: {
Toast: (x: any) => `مع ${x} اقتراحًا ذا سياق`, Toast: (x: any) => `يحتوي على ${x} إشعارات مخصصة`,
Edit: "الاقتراحات السياقية والذاكرة", Edit: "إعدادات الدردشة الحالية",
Add: "إضافة اقتراح", Add: "إضافة دردشة جديدة",
Clear: "مسح السياق", Clear: "تم مسح السياق",
Revert: "التراجع", Revert: "استعادة السياق",
}, },
Plugin: { Plugin: {
Name: "المكوّن الإضافي", Name: "الإضافات",
}, },
FineTuned: { FineTuned: {
Sysmessage: "أنت مساعد ي", Sysmessage: "أنت مساعد",
}, },
Mask: { SearchChat: {
Name: "الأقنعة", Name: "بحث",
Page: { Page: {
Title: "قالب الاقتراح", Title: "البحث في سجلات الدردشة",
SubTitle: (count: number) => `${count} قوالب الاقتراح`, Search: "أدخل كلمات البحث",
Search: "البحث في القوالب", NoResult: "لم يتم العثور على نتائج",
Create: "إنشاء", NoData: "لا توجد بيانات",
Loading: "جارٍ التحميل",
SubTitle: (count: number) => `تم العثور على ${count} نتائج`,
}, },
Item: { Item: {
Info: (count: number) => `${count} اقتراحات`, View: "عرض",
},
},
Mask: {
Name: "القناع",
Page: {
Title: "أقنعة الأدوار المخصصة",
SubTitle: (count: number) => `${count} تعريف لدور مخصص`,
Search: "بحث عن قناع الدور",
Create: "إنشاء جديد",
},
Item: {
Info: (count: number) => `يحتوي على ${count} محادثات مخصصة`,
Chat: "الدردشة", Chat: "الدردشة",
View: "عرض", View: "عرض",
Edit: "تعديل", Edit: حرير",
Delete: "حذف", Delete: "حذف",
DeleteConfirm: "تأكيد الحذف؟", DeleteConfirm: "تأكيد الحذف؟",
}, },
EditModal: { EditModal: {
Title: (readonly: boolean) => ` Title: (readonly: boolean) =>
تعديل قالب الاقتراح ${readonly ? "(للقراءة فقط)" : ""}`, `تحرير القناع المخصص ${readonly ? " (للقراءة فقط)" : ""}`,
Download: "تنزيل", Download: "تنزيل القناع المخصص",
Clone: "استنساخ", Clone: "استنساخ القناع",
}, },
Config: { Config: {
Avatar: "صورة الروبوت", Avatar: "صورة الدور",
Name: "اسم الروبوت", Name: "اسم الدور",
Sync: { Sync: {
Title: "استخدام الإعدادات العامة", Title: "استخدام الإعدادات العالمية",
SubTitle: "استخدام الإعدادات العامة في هذه الدردشة", SubTitle: "هل تستخدم الدردشة الحالية الإعدادات العالمية للنموذج",
Confirm: "تأكيد الاستبدال بالإعدادات المخصصة بالإعدادات العامة؟", Confirm:
"ستتم الكتابة فوق الإعدادات المخصصة للدردشة الحالية تلقائيًا، تأكيد تفعيل الإعدادات العالمية؟",
}, },
HideContext: { HideContext: {
Title: "إخفاء اقتراحات السياق", Title: "إخفاء المحادثات المخصصة",
SubTitle: "عدم عرض اقتراحات السياق في الدردشة", SubTitle: "بعد الإخفاء، لن تظهر المحادثات المخصصة في واجهة الدردشة",
},
Share: {
Title: "مشاركة هذا القناع",
SubTitle: "إنشاء رابط مباشر لهذا القناع",
Action: "نسخ الرابط",
}, },
}, },
}, },
NewChat: { NewChat: {
Return: "العودة", Return: "العودة",
Skip: "ابدأ فقط", Skip: "بدء الآن",
Title: "اختيار قناع",
SubTitle: "دردشة مع الروح وراء القناع",
More: "المزيد",
NotShow: "عدم العرض مرة أخرى", NotShow: "عدم العرض مرة أخرى",
ConfirmNoShow: "تأكيد تعطيله؟ يمكنك تمكينه في الإعدادات لاحقًا.", ConfirmNoShow:
"تأكيد إلغاء العرض؟ بعد الإلغاء، يمكنك إعادة تفعيله في الإعدادات في أي وقت.",
Title: "اختر قناعًا",
SubTitle: "ابدأ الآن وتفاعل مع الأفكار خلف القناع",
More: "عرض الكل",
},
URLCommand: {
Code: "تم الكشف عن رمز وصول في الرابط، هل تريد تعبئته تلقائيًا؟",
Settings: "تم الكشف عن إعدادات مسبقة في الرابط، هل تريد تعبئتها تلقائيًا؟",
}, },
UI: { UI: {
@ -276,9 +560,16 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
Cancel: "إلغاء", Cancel: "إلغاء",
Close: "إغلاق", Close: "إغلاق",
Create: "إنشاء", Create: "إنشاء",
Edit: "تعديل", Edit: "تحرير",
Export: "تصدير",
Import: "استيراد",
Sync: "مزامنة",
Config: "تكوين",
}, },
Exporter: { Exporter: {
Description: {
Title: "فقط الرسائل بعد مسح السياق سيتم عرضها",
},
Model: "النموذج", Model: "النموذج",
Messages: "الرسائل", Messages: "الرسائل",
Topic: "الموضوع", Topic: "الموضوع",

View File

@ -1,332 +1,588 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import { PartialLocaleType } from "./index"; import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
const bn: PartialLocaleType = { const bn: PartialLocaleType = {
WIP: "শীঘ্রই আসছে...", WIP: "শীঘ্রই আসছে...",
Error: { Error: {
Unauthorized: Unauthorized: isApp
"অননুমোদিত অ্যাক্সেস, অনুগ্রহ করে [অথোরাইজশন](/#/auth) পৃষ্ঠায় অ্যাক্সেস কোড ইনপুট করুন।", ? `😆 কথোপকথনে কিছু সমস্যা হয়েছে, চিন্তার কিছু নেই:
\\ 1 ি ি ি , [ ি ি 🚀](${SAAS_CHAT_UTM_URL})
\\ 2 ি ি ি OpenAI , [ ি ](/#/settings) ি ি `
: `😆 কথোপকথনে কিছু সমস্যা হয়েছে, চিন্তার কিছু নেই:
\ 1 ি ি ি , [ ি ি 🚀](${SAAS_CHAT_UTM_URL})
\ 2 ি ি ি , [ ি ](/#/auth) ি ি 🔑
\ 3 ি ি ি OpenAI , [ ি ](/#/settings) ি ি
`,
}, },
Auth: { Auth: {
Title: "একটি অ্যাক্সেস কোড প্রয়োজন", Title: "পাসওয়ার্ড প্রয়োজন",
Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন", Tips: "অ্যাডমিন পাসওয়ার্ড প্রমাণীকরণ চালু করেছেন, নিচে অ্যাক্সেস কোড প্রবেশ করুন",
SubTips: "অথবা আপনার OpenAI API কী প্রবেশ করুন", SubTips: "অথবা আপনার OpenAI অথবা Google API কী প্রবেশ করান",
Input: "অ্যাক্সেস কোড", Input: "এখানে অ্যাক্সেস কোড লিখুন",
Confirm: "নিশ্চিত করুন", Confirm: "নিশ্চিত করুন",
Later: "পরে", Later: "পরে বলুন",
Return: "ফিরে আসা",
SaasTips: "কনফিগারেশন খুব কঠিন, আমি অবিলম্বে ব্যবহার করতে চাই",
TopTips:
"🥳 NextChat AI প্রথম প্রকাশের অফার, এখনই OpenAI o1, GPT-4o, Claude-3.5 এবং সর্বশেষ বড় মডেলগুলি আনলক করুন",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} টি বার্তা`, ChatItemCount: (count: number) => `${count} টি চ্যাট`,
}, },
Chat: { Chat: {
SubTitle: (count: number) => `${count} টি বার্তা`, SubTitle: (count: number) => `মোট ${count} টি চ্যাট`,
EditMessage: {
Title: "বার্তাগুলি সম্পাদনা করুন",
Topic: {
Title: "চ্যাটের বিষয়",
SubTitle: "বর্তমান চ্যাটের বিষয় পরিবর্তন করুন",
},
},
Actions: { Actions: {
ChatList: "চ্যাট তালিকায় যান", ChatList: "বার্তা তালিকা দেখুন",
CompressedHistory: "সংক্ষিপ্ত ইতিহাস মেমোরি প্রম্পট", CompressedHistory: "সংকুচিত ইতিহাস দেখুন",
Export: "সমস্ত বার্তা মার্কডাউন হিসাবে রপ্তানি করুন", Export: "চ্যাট ইতিহাস রপ্তানী করুন",
Copy: "কপি", Copy: "অনুলিপি করুন",
Stop: "বন্ধ করুন", Stop: "থামান",
Retry: "পুনরায় চেষ্টা করুন", Retry: "পুনরায় চেষ্টা করুন",
Pin: "পিন করুন", Pin: "পিন করুন",
PinToastContent: "পিন করা হয়েছে ২টি বার্তা প্রম্পটে", PinToastContent: "1 টি চ্যাট পূর্বনির্ধারিত প্রম্পটে পিন করা হয়েছে",
PinToastAction: "দেখুন", PinToastAction: "দেখুন",
Delete: "মুছে ফেলুন", Delete: "মুছে ফেলুন",
Edit: "সম্পাদন করুন", Edit: "সম্পাদনা করুন",
RefreshTitle: "শিরোনাম রিফ্রেশ করুন",
RefreshToast: "শিরোনাম রিফ্রেশ অনুরোধ পাঠানো হয়েছে",
}, },
Commands: { Commands: {
new: "নতুন চ্যাট শুরু করুন", new: "নতুন চ্যাট",
newm: "মাস্ক সহ নতুন চ্যাট শুরু করুন", newm: "মাস্ক থেকে নতুন চ্যাট",
next: "পরবর্তী চ্যাট", next: "পরবর্তী চ্যাট",
prev: "পূর্ববর্তী চ্যাট", prev: "পূর্ববর্তী চ্যাট",
clear: "সংশ্লিষ্টতাবদ্ধকরণ পরিষ্কার করুন", clear: "প্রসঙ্গ পরিষ্কার করুন",
del: "চ্যাট মুছুন", del: "চ্যাট মুছে ফেলুন",
}, },
InputActions: { InputActions: {
Stop: "বন্ধ করুন", Stop: "প্রতিক্রিয়া থামান",
ToBottom: "সর্বশেষতম দিকে", ToBottom: "সর্বশেষে স্ক্রোল করুন",
Theme: { Theme: {
auto: "অটো", auto: "স্বয়ংক্রিয় থিম",
light: "হালকা থিম", light: "আলোর মোড",
dark: "ডার্ক থিম", dark: "অন্ধকার মোড",
}, },
Prompt: "প্রম্পটগুলিতে", Prompt: "সংক্ষিপ্ত নির্দেশনা",
Masks: "মাস্কগুলি", Masks: "সমস্ত মাস্ক",
Clear: "সংশ্লিষ্টতাবদ্ধকরণ পরিষ্কার করুন", Clear: "চ্যাট পরিষ্কার করুন",
Settings: "সেটিংস", Settings: "চ্যাট সেটিংস",
UploadImage: "চিত্র আপলোড করুন",
}, },
Rename: "চ্যাট পুনঃনামকরণ করুন", Rename: "চ্যাট নাম পরিবর্তন করুন",
Typing: "টাইপিং...", Typing: "লিখছে…",
Input: (submitKey: string) => { Input: (submitKey: string) => {
var inputHints = `${submitKey} to send`; var inputHints = `${submitKey} পাঠান`;
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter to wrap"; inputHints += "Shift + Enter নতুন লাইন";
} }
return inputHints + ", / to search prompts, : to use commands"; return inputHints + "/ পূর্ণতা সক্রিয় করুন,: কমান্ড সক্রিয় করুন";
}, },
Send: "প্রেরণ করুন", Send: "পাঠান",
Config: { Config: {
Reset: "ডিফল্টে রিসেট করুন", Reset: "মেমরি মুছে ফেলুন",
SaveAs: "মাস্ক হিসাবে সংরক্ষণ করুন", SaveAs: "মাস্ক হিসাবে সংরক্ষণ করুন",
}, },
IsContext: "পূর্বনির্ধারিত প্রম্পট",
}, },
Export: { Export: {
Title: "বার্তা রপ্তানিকরণ", Title: "চ্যাট ইতিহাস শেয়ার করুন",
Copy: "সমস্তটি কপি করুন", Copy: "সবকিছু কপি করুন",
Download: "ডাউনলোড করুন", Download: "ফাইল ডাউনলোড করুন",
MessageFromYou: "আপনার বার্তা", Share: "ShareGPT তে শেয়ার করুন",
MessageFromChatGPT: "চ্যাটজিপিটির বার্তা", MessageFromYou: "ব্যবহারকারী",
Share: "শেয়ার করুন শেয়ারজিপিটি তে", MessageFromChatGPT: "ChatGPT",
Format: { Format: {
Title: "রপ্তানি ফরম্যাট", Title: "রপ্তানি ফরম্যাট",
SubTitle: "মার্কডাউন বা পিএনজি চিত্র", SubTitle: "Markdown টেক্সট বা PNG চিত্র রপ্তানি করা যাবে",
}, },
IncludeContext: { IncludeContext: {
Title: "মাস্ক অন্তর্ভুক্ত করুন", Title: "মাস্ক প্রসঙ্গ অন্তর্ভুক্ত করুন",
SubTitle: "মাস্কগুলি সংরক্ষণ করবেন না কি", SubTitle: "বার্তায় মাস্ক প্রসঙ্গ প্রদর্শন করা হবে কি না",
}, },
Steps: { Steps: {
Select: "নির্বাচন করুন", Select: "নির্বাচন করুন",
Preview: "প্রিভিউ করুন", Preview: "পূর্বরূপ দেখুন",
},
Image: {
Toast: "স্ক্রীনশট তৈরি করা হচ্ছে",
Modal: "ছবি সংরক্ষণ করতে দীর্ঘ প্রেস করুন অথবা রাইট ক্লিক করুন",
}, },
}, },
Select: { Select: {
Search: "অনুসন্ধান করুন", Search: "বার্তা অনুসন্ধান করুন",
All: "সমস্তটি নির্বাচন করুন", All: "সবকিছু নির্বাচন করুন",
Latest: "সর্বশেষতমটি নির্বাচন করুন", Latest: "সর্বশেষ কিছু",
Clear: "পরিষ্কার করুন", Clear: "নির্বাচন পরিষ্কার করুন",
}, },
Memory: { Memory: {
Title: "মেমোরি প্রম্পট", Title: "ইতিহাস সারাংশ",
EmptyContent: "এখনও কিছুই নেই।", EmptyContent: "চ্যাটের বিষয়বস্তু খুব সংক্ষিপ্ত, সারাংশ প্রয়োজন নেই",
Send: "মেমোরি প্রেরণ করুন", Send: "অটোমেটিক চ্যাট ইতিহাস সংকুচিত করুন এবং প্রসঙ্গ হিসেবে পাঠান",
Copy: "মেমোরি কপি করুন", Copy: "সারাংশ কপি করুন",
Reset: "পুনরায় নিশ্চিত করুন", Reset: "[unused]",
ResetConfirm: ResetConfirm: "ইতিহাস সারাংশ মুছে ফেলার নিশ্চিত করুন?",
"রিসেট করলে বর্তমান চ্যাট ইতিহাস এবং ঐতিহাসিক মেমোরি মুছে যাবে। পুনরায় নির্দিষ্ট করতে চান তা নিশ্চিত করতে চান?",
}, },
Home: { Home: {
NewChat: "নতুন চ্যাট", NewChat: "নতুন চ্যাট",
DeleteChat: "নির্বাচিত সংলাপটি মুছতে নিশ্চিত করুন?", DeleteChat: "নির্বাচিত চ্যাট মুছে ফেলার নিশ্চিত করুন?",
DeleteToast: "চ্যাটটি মুছেছে", DeleteToast: "চ্যাট মুছে ফেলা হয়েছে",
Revert: "পুনরায়", Revert: "পূর্বাবস্থায় ফেরান",
}, },
Settings: { Settings: {
Title: "সেটিংস", Title: "সেটিংস",
SubTitle: "সমস্ত সেটিংস", SubTitle: "সমস্ত সেটিংস অপশন",
Danger: { Danger: {
Reset: { Reset: {
Title: "সমস্ত সেটিংস পুনঃনির্দেশ দিন", Title: "সমস্ত সেটিংস পুনরায় সেট করুন",
SubTitle: "সকল সেটিংস ডিফল্টে পুনঃনির্দেশ দিতে", SubTitle: "সমস্ত সেটিংস বিকল্পগুলিকে ডিফল্ট মানে পুনরায় সেট করুন",
Action: "পুনঃনির্দেশ দিন", Action: "এখনই পুনরায় সেট করুন",
Confirm: "সমস্ত সেটিংস ডিফল্টে পুনঃনির্দেশ করতে নিশ্চিত করতে?", Confirm: "সমস্ত সেটিংস পুনরায় সেট করার নিশ্চিত করুন?",
}, },
Clear: { Clear: {
Title: "সমস্ত তথ্য মুছুন", Title: "সমস্ত তথ্য মুছে ফেলুন",
SubTitle: "সমস্ত বার্তা এবং সেটিংস মুছুন", SubTitle: "সমস্ত চ্যাট এবং সেটিংস ডেটা মুছে ফেলুন",
Action: "মুছুন", Action: "এখনই মুছে ফেলুন",
Confirm: "সমস্ত বার্তা এবং সেটিংস মুছে ফেলতে নিশ্চিত করতে?", Confirm: "সমস্ত চ্যাট এবং সেটিংস ডেটা মুছে ফেলানোর নিশ্চিত করুন?",
}, },
}, },
Lang: { Lang: {
Name: "বাংলা", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "সমস্ত ভাষা", All: "সমস্ত ভাষা",
}, },
Avatar: "অবতার", Avatar: "অভিনেতা",
FontSize: { FontSize: {
Title: "ফন্ট সাইজ", Title: "ফন্ট সাইজ",
SubTitle: "চ্যাট সামগ্রীর ফন্ট সাইজ সংশোধন করুন", SubTitle: "চ্যাট কনটেন্টের ফন্ট সাইজ",
},
FontFamily: {
Title: "চ্যাট ফন্ট",
SubTitle:
"চ্যাট সামগ্রীর ফন্ট, বিশ্বব্যাপী ডিফল্ট ফন্ট প্রয়োগ করতে খালি রাখুন",
Placeholder: "ফন্টের নাম",
}, },
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "حقن تلميحات النظام", Title: "সিস্টেম-লেভেল প্রম্পট যোগ করুন",
SubTitle: SubTitle:
"قم بإضافة تلميحة نظام محاكاة ChatGPT إلى بداية قائمة الرسائل المُطلَبة في كل طلب", "প্রত্যেক বার্তায় একটি সিস্টেম প্রম্পট যোগ করুন যা ChatGPT এর অনুকরণ করবে",
}, },
InputTemplate: { InputTemplate: {
Title: "ইনপুট টেমপ্লেট", Title: "ব্যবহারকারীর ইনপুট প্রিপ্রসেসিং",
SubTitle: "নতুনতম বার্তা এই টেমপ্লেটে পূরণ হবে", SubTitle: "ব্যবহারকারীর সর্বশেষ বার্তা এই টেমপ্লেটে পূরণ করা হবে",
}, },
Update: { Update: {
Version: (x: string) => `Version: ${x}`, Version: (x: string) => `বর্তমান সংস্করণ: ${x}`,
IsLatest: "Latest version", IsLatest: "এটি সর্বশেষ সংস্করণ",
CheckUpdate: "Check Update", CheckUpdate: "আপডেট পরীক্ষা করুন",
IsChecking: "Checking update...", IsChecking: "আপডেট পরীক্ষা করা হচ্ছে...",
FoundUpdate: (x: string) => `Found new version: ${x}`, FoundUpdate: (x: string) => `নতুন সংস্করণ পাওয়া গিয়েছে: ${x}`,
GoToUpdate: "Update", GoToUpdate: "আপডেট করতে যান",
}, },
SendKey: "প্রেরণ চাবি", SendKey: "পাঠানোর কী",
Theme: "থিম", Theme: "থিম",
TightBorder: "সঙ্গতি সীমা", TightBorder: "বর্ডার-বিহীন মোড",
SendPreviewBubble: { SendPreviewBubble: {
Title: "প্রিভিউ বুলবুল প্রেরণ করুন", Title: "প্রিভিউ বুদবুদ",
SubTitle: "বুলবুলে মার্কডাউন প্রিভিউ করুন", SubTitle: "প্রিভিউ বুদবুদে Markdown কনটেন্ট প্রিভিউ করুন",
},
AutoGenerateTitle: {
Title: "স্বয়ংক্রিয় শিরোনাম জেনারেশন",
SubTitle: "চ্যাট কনটেন্টের ভিত্তিতে উপযুক্ত শিরোনাম তৈরি করুন",
},
Sync: {
CloudState: "ক্লাউড ডেটা",
NotSyncYet: "এখনো সিঙ্ক করা হয়নি",
Success: "সিঙ্ক সফল",
Fail: "সিঙ্ক ব্যর্থ",
Config: {
Modal: {
Title: "ক্লাউড সিঙ্ক কনফিগার করুন",
Check: "পরীক্ষা করুন",
},
SyncType: {
Title: "সিঙ্ক টাইপ",
SubTitle: "পছন্দসই সিঙ্ক সার্ভার নির্বাচন করুন",
},
Proxy: {
Title: "প্রক্সি সক্রিয় করুন",
SubTitle:
"ব্রাউজারে সিঙ্ক করার সময়, ক্রস-অরিজিন সীমাবদ্ধতা এড়াতে প্রক্সি সক্রিয় করতে হবে",
},
ProxyUrl: {
Title: "প্রক্সি ঠিকানা",
SubTitle:
"এটি শুধুমাত্র প্রকল্পের সাথে সরবরাহিত ক্রস-অরিজিন প্রক্সির জন্য প্রযোজ্য",
},
WebDav: {
Endpoint: "WebDAV ঠিকানা",
UserName: "ব্যবহারকারীর নাম",
Password: "পাসওয়ার্ড",
},
UpStash: {
Endpoint: "UpStash Redis REST URL",
UserName: "ব্যাকআপ নাম",
Password: "UpStash Redis REST টোকেন",
},
},
LocalState: "স্থানীয় ডেটা",
Overview: (overview: any) => {
return `${overview.chat} বার চ্যাট, ${overview.message} বার্তা, ${overview.prompt} প্রম্পট, ${overview.mask} মাস্ক`;
},
ImportFailed: "আমদানি ব্যর্থ",
}, },
Mask: { Mask: {
Splash: { Splash: {
Title: "মাস্ক স্প্ল্যাশ স্ক্রিন", Title: "মাস্ক লঞ্চ পেজ",
SubTitle: SubTitle: "নতুন চ্যাট শুরু করার সময় মাস্ক লঞ্চ পেজ প্রদর্শন করুন",
"নতুন চ্যাট শুরু করার আগে মাস্ক স্প্ল্যাশ স্ক্রিন প্রদর্শন করুন",
}, },
Builtin: { Builtin: {
Title: "মূলত মাস্ক গোপন করুন", Title: "ইনবিল্ট মাস্ক লুকান",
SubTitle: "মাস্ক তালিকা থেকে মূলত মাস্কগুলি লুকান", SubTitle: "সমস্ত মাস্ক তালিকায় ইনবিল্ট মাস্ক লুকান",
}, },
}, },
Prompt: { Prompt: {
Disable: { Disable: {
Title: "অটো-সম্পূর্ণতা নিষ্ক্রিয় করুন", Title: "প্রম্পট অটো-কমপ্লিশন নিষ্ক্রিয় করুন",
SubTitle: "অটো-সম্পূর্ণতা চালু করতে / ইনপুট করুন", SubTitle: "ইনপুট বক্সের শুরুতে / টাইপ করলে অটো-কমপ্লিশন সক্রিয় হবে",
}, },
List: "প্রম্পট তালিকা", List: "স্বনির্ধারিত প্রম্পট তালিকা",
ListCount: (builtin: number, custom: number) => ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`, `ইনবিল্ট ${builtin} টি, ব্যবহারকারী সংজ্ঞায়িত ${custom} টি`,
Edit: "সম্পাদন করুন", Edit: "সম্পাদন করুন",
Modal: { Modal: {
Title: "প্রম্পট তালিকা", Title: "প্রম্পট তালিকা",
Add: "একটি যোগ করুন", Add: "নতুন করুন",
Search: "সন্ধান প্রম্পট", Search: "প্রম্পট অনুসন্ধান করুন",
}, },
EditModal: { EditModal: {
Title: "সম্পাদন করুন প্রম্পট", Title: "প্রম্পট সম্পাদন করুন",
}, },
}, },
HistoryCount: { HistoryCount: {
Title: "সংযুক্ত বার্তা সংখ্যা", Title: "সংযুক্ত ইতিহাস বার্তা সংখ্যা",
SubTitle: "প্রতি অনুরোধে প্রেরণ করা গেলে প্রেরণ করা হবে", SubTitle: "প্রতিটি অনুরোধে সংযুক্ত ইতিহাস বার্তার সংখ্যা",
}, },
CompressThreshold: { CompressThreshold: {
Title: "ইতিহাস সঙ্কুচিত করার সীমা", Title: "ইতিহাস বার্তা দৈর্ঘ্য সংকুচিত থ্রেশহোল্ড",
SubTitle: SubTitle:
"নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে", "যখন সংকুচিত ইতিহাস বার্তা এই মান ছাড়িয়ে যায়, তখন সংকুচিত করা হবে",
}, },
Usage: { Usage: {
Title: "একাউন্ট ব্যালেন্স", Title: "ব্যালেন্স চেক",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
return `এই মাসে ব্যবহৃত $${used}, সাবস্ক্রিপশন $${total}`; return `এই মাসে ব্যবহৃত $${used}, সাবস্ক্রিপশন মোট $${total}`;
}, },
IsChecking: "চেক করা হচ্ছে...", IsChecking: "পরীক্ষা করা হচ্ছে…",
Check: "চেক", Check: "পুনরায় পরীক্ষা করুন",
NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন", NoAccess: "ব্যালেন্স দেখতে API কী অথবা অ্যাক্সেস পাসওয়ার্ড প্রবেশ করুন",
}, },
Model: "মডেল", Access: {
SaasStart: {
Title: "NextChat AI ব্যবহার করুন",
Label: "(সেরা মূল্যসাশ্রয়ী সমাধান)",
SubTitle:
"NextChat কর্তৃক অফিসিয়াল রক্ষণাবেক্ষণ, শূন্য কনফিগারেশন ব্যবহার শুরু করুন, OpenAI o1, GPT-4o, Claude-3.5 সহ সর্বশেষ বড় মডেলগুলি সমর্থন করে",
ChatNow: "এখনই চ্যাট করুন",
},
AccessCode: {
Title: "অ্যাক্সেস পাসওয়ার্ড",
SubTitle: "অ্যাডমিন এনক্রিপ্টেড অ্যাক্সেস সক্রিয় করেছেন",
Placeholder: "অ্যাক্সেস পাসওয়ার্ড প্রবেশ করুন",
},
CustomEndpoint: {
Title: "স্বনির্ধারিত ইন্টারফেস",
SubTitle: "স্বনির্ধারিত Azure বা OpenAI সার্ভিস ব্যবহার করবেন কি?",
},
Provider: {
Title: "মডেল পরিষেবা প্রদানকারী",
SubTitle: "বিভিন্ন পরিষেবা প্রদানকারীতে স্যুইচ করুন",
},
OpenAI: {
ApiKey: {
Title: "API কী",
SubTitle:
"পাসওয়ার্ড অ্যাক্সেস সীমাবদ্ধতা এড়াতে স্বনির্ধারিত OpenAI কী ব্যবহার করুন",
Placeholder: "OpenAI API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "ডিফল্ট ঠিকানা বাদে, http(s):// অন্তর্ভুক্ত করতে হবে",
},
},
Azure: {
ApiKey: {
Title: "ইন্টারফেস কী",
SubTitle:
"পাসওয়ার্ড অ্যাক্সেস সীমাবদ্ধতা এড়াতে স্বনির্ধারিত Azure কী ব্যবহার করুন",
Placeholder: "Azure API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "উদাহরণ:",
},
ApiVerion: {
Title: "ইন্টারফেস সংস্করণ (azure api version)",
SubTitle: "নির্দিষ্ট সংস্করণ নির্বাচন করুন",
},
},
Anthropic: {
ApiKey: {
Title: "ইন্টারফেস কী",
SubTitle:
"পাসওয়ার্ড অ্যাক্সেস সীমাবদ্ধতা এড়াতে স্বনির্ধারিত Anthropic কী ব্যবহার করুন",
Placeholder: "Anthropic API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "উদাহরণ:",
},
ApiVerion: {
Title: "ইন্টারফেস সংস্করণ (claude api version)",
SubTitle: "নির্দিষ্ট API সংস্করণ প্রবেশ করুন",
},
},
Google: {
ApiKey: {
Title: "API কী",
SubTitle: "Google AI থেকে আপনার API কী পান",
Placeholder: "আপনার Google AI Studio API কী প্রবেশ করুন",
},
Endpoint: {
Title: "টার্মিনাল ঠিকানা",
SubTitle: "উদাহরণ:",
},
ApiVersion: {
Title: "API সংস্করণ (শুধুমাত্র gemini-pro)",
SubTitle: "একটি নির্দিষ্ট API সংস্করণ নির্বাচন করুন",
},
GoogleSafetySettings: {
Title: "Google সেফটি ফিল্টার স্তর",
SubTitle: "বিষয়বস্তু ফিল্টার স্তর সেট করুন",
},
},
Baidu: {
ApiKey: {
Title: "API কী",
SubTitle: "স্বনির্ধারিত Baidu API কী ব্যবহার করুন",
Placeholder: "Baidu API কী",
},
SecretKey: {
Title: "সিক্রেট কী",
SubTitle: "স্বনির্ধারিত Baidu সিক্রেট কী ব্যবহার করুন",
Placeholder: "Baidu সিক্রেট কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "স্বনির্ধারিত সমর্থিত নয়, .env কনফিগারেশনে চলে যান",
},
},
ByteDance: {
ApiKey: {
Title: "ইন্টারফেস কী",
SubTitle: "স্বনির্ধারিত ByteDance API কী ব্যবহার করুন",
Placeholder: "ByteDance API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "উদাহরণ:",
},
},
Alibaba: {
ApiKey: {
Title: "ইন্টারফেস কী",
SubTitle: "স্বনির্ধারিত আলিবাবা ক্লাউড API কী ব্যবহার করুন",
Placeholder: "Alibaba Cloud API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "উদাহরণ:",
},
},
CustomModel: {
Title: "স্বনির্ধারিত মডেল নাম",
SubTitle:
"স্বনির্ধারিত মডেল বিকল্পগুলি যুক্ত করুন, ইংরেজি কমা দ্বারা আলাদা করুন",
},
},
Model: "মডেল (model)",
CompressModel: {
Title: "সংকোচন মডেল",
SubTitle: "ইতিহাস সংকুচিত করার জন্য ব্যবহৃত মডেল",
},
Temperature: { Temperature: {
Title: "তাপমাত্রা", Title: "যাদুকরিতা (temperature)",
SubTitle: "আরতি মান বেশি করলে বেশি এলোমেলো আউটপুট হবে", SubTitle: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে",
}, },
TopP: { TopP: {
Title: "শীর্ষ পি", Title: "নিউক্লিয়ার স্যাম্পলিং (top_p)",
SubTitle: "তাপমাত্রা সঙ্গে এই মান পরিবর্তন করবেন না", SubTitle: "যাদুকরিতা মত, কিন্তু একসাথে পরিবর্তন করবেন না",
}, },
MaxTokens: { MaxTokens: {
Title: "সর্বাধিক টোকেন", Title: "একটি উত্তর সীমা (max_tokens)",
SubTitle: "ইনপুট টোকেন এবং উৎপাদিত টোকেনের সর্বাধিক দৈর্ঘ্য", SubTitle: "প্রতি ইন্টারঅ্যাকশনে সর্বাধিক টোকেন সংখ্যা",
}, },
PresencePenalty: { PresencePenalty: {
Title: "উপস্থিতির জরিমানা", Title: "বিষয়বস্তু তাজা (presence_penalty)",
SubTitle: "আরতি মান বেশি করলে নতুন বিষয়গুলি সম্ভাব্যতা বাড়াতে পারে", SubTitle: "মান বাড়ালে নতুন বিষয়ে প্রসারিত হওয়ার সম্ভাবনা বেশি",
}, },
FrequencyPenalty: { FrequencyPenalty: {
Title: "ফ্রিকুয়েন্সি জরিমানা", Title: "ফ্রিকোয়েন্সি পেনাল্টি (frequency_penalty)",
SubTitle: SubTitle: "মান বাড়ালে পুনরাবৃত্তি শব্দ কমানোর সম্ভাবনা বেশি",
"আরতি মান বাড়ালে একই লাইন পুনরায় ব্যাবহার করার সম্ভাবনা হ্রাস পায়",
}, },
}, },
Store: { Store: {
DefaultTopic: "নতুন সংলাপ", DefaultTopic: "নতুন চ্যাট",
BotHello: "হ্যালো! আজকে আপনাকে কিভাবে সাহায্য করতে পারি?", BotHello: "আপনার জন্য কিছু করতে পারি?",
Error: "কিছু নিয়ে ভুল হয়েছে, পরে আবার চেষ্টা করুন।", Error: "একটি ত্রুটি ঘটেছে, পরে আবার চেষ্টা করুন",
Prompt: { Prompt: {
History: (content: string) => History: (content: string) =>
"এটি চ্যাট ইতিহাসের সংক্ষিপ্ত সংকলনের মতো: " + content, "এটি পূর্বের চ্যাটের সারাংশ হিসেবে ব্যবহৃত হবে: " + content,
Topic: Topic:
"আমাদের সংলাপটির চার থেকে পাঁচ শব্দের একটি শিরোনাম তৈরি করুন যা আমাদের আলাপের সংক্ষিপ্তসার হিসাবে যোগ হবে না, যেমন অভিবৃত্তি, বিন্যাস, উদ্ধৃতি, পূর্বচালক চিহ্ন, পূর্বরোবক্তির যেকোনো চিহ্ন বা অতিরিক্ত পাঠ। মেয়াদশেষ উদ্ধৃতি চেষ্টা করুন।", "চার থেকে পাঁচটি শব্দ ব্যবহার করে এই বাক্যের সংক্ষিপ্ত থিম দিন, ব্যাখ্যা, বিরাম চিহ্ন, ভাষা, অতিরিক্ত টেক্সট বা বোল্ড না ব্যবহার করুন। যদি কোনো থিম না থাকে তবে সরাসরি 'বেকার' বলুন",
Summarize: Summarize:
"২০০ শব্দের লম্বা হয়ে মুহূর্তে আলোচনা সংক্ষেপের রপ্তানি করুন, যেটি ভবিষ্যতের প্রম্পট হিসাবে ব্যবহার করবেন।", "আলোচনার বিষয়বস্তু সংক্ষিপ্তভাবে সারাংশ করুন, পরবর্তী কনটেক্সট প্রম্পট হিসেবে ব্যবহারের জন্য, ২০০ শব্দের মধ্যে সীমাবদ্ধ রাখুন",
}, },
}, },
Copy: { Copy: {
Success: "ক্লিপবোর্ডে কপি করা হয়েছে", Success: "ক্লিপবোর্ডে লেখা হয়েছে",
Failed: "কপি ব্যর্থ, অনুমতি প্রদান করার জন্য অনুমতি প্রদান করুন", Failed: "কপি ব্যর্থ হয়েছে, দয়া করে ক্লিপবোর্ড অনুমতি প্রদান করুন",
},
Download: {
Success: "বিষয়বস্তু আপনার ডিরেক্টরিতে ডাউনলোড করা হয়েছে।",
Failed: "ডাউনলোড ব্যর্থ হয়েছে।",
}, },
Context: { Context: {
Toast: (x: any) => `With ${x} contextual prompts`, Toast: (x: any) => `${x}টি পূর্বনির্ধারিত প্রম্পট অন্তর্ভুক্ত`,
Edit: "বর্তমান চ্যাট সেটিংস", Edit: "বর্তমান চ্যাট সেটিংস",
Add: "একটি প্রম্পট যোগ করুন", Add: "একটি নতুন চ্যাট যোগ করুন",
Clear: "সঙ্গতি পরিস্কার করুন", Clear: "কনটেক্সট পরিষ্কার করা হয়েছে",
Revert: "পূর্ববর্তী অবস্থানে ফিরে যান", Revert: "কনটেক্সট পুনরুদ্ধার করুন",
}, },
Plugin: { Plugin: {
Name: "প্লাগইন", Name: "প্লাগইন",
}, },
FineTuned: { FineTuned: {
Sysmessage: "আপনি একটি সহকারী যা", Sysmessage: "আপনি একজন সহকারী",
},
SearchChat: {
Name: "অনুসন্ধান",
Page: {
Title: "চ্যাট রেকর্ড অনুসন্ধান করুন",
Search: "অনুসন্ধান কীওয়ার্ড লিখুন",
NoResult: "কোন ফলাফল পাওয়া যায়নি",
NoData: "কোন তথ্য নেই",
Loading: "লোড হচ্ছে",
SubTitle: (count: number) => `${count} টি ফলাফল পাওয়া গেছে`,
},
Item: {
View: "দেখুন",
},
}, },
Mask: { Mask: {
Name: "মাস্ক", Name: "মাস্ক",
Page: { Page: {
Title: "প্রম্পট টেমপ্লেট", Title: "পূর্বনির্ধারিত চরিত্র মাস্ক",
SubTitle: (count: number) => `${count} টি প্রম্পট টেমপ্লেট`, SubTitle: (count: number) => `${count}টি পূর্বনির্ধারিত চরিত্র সংজ্ঞা`,
Search: "টেমপ্লেট অনুসন্ধান করুন", Search: "চরিত্র মাস্ক অনুসন্ধান করুন",
Create: "তৈরি করুন", Create: "নতুন তৈরি করুন",
}, },
Item: { Item: {
Info: (count: number) => `${count} প্রম্পট`, Info: (count: number) => `ভিতরে ${count}টি পূর্বনির্ধারিত চ্যাট রয়েছে`,
Chat: "চ্যাট", Chat: "চ্যাট",
View: "দেখুন", View: "দেখুন",
Edit: "সম্পাদন করুন", Edit: "সম্পাদন করুন",
Delete: "মুছে ফেলুন", Delete: "মুছে ফেলুন",
DeleteConfirm: "মুছে ফেলতে নিশ্চিত করুন?", DeleteConfirm: "মুছে ফেলার জন্য নিশ্চিত করুন?",
}, },
EditModal: { EditModal: {
Title: (readonly: boolean) => Title: (readonly: boolean) =>
`প্রম্পট টেমপ্লেট সম্পাদন করুন ${readonly ? "(readonly)" : ""}`, `ূর্বনির্ধারিত মাস্ক সম্পাদনা ${readonly ? "(পঠনযোগ্য)" : ""}`,
Download: "ডাউনলোড করুন", Download: "পূর্বনির্ধারিত ডাউনলোড করুন",
Clone: "ক্লোন করুন", Clone: "পূর্বনির্ধারিত ক্লোন করুন",
}, },
Config: { Config: {
Avatar: "বট অবতার", Avatar: "চরিত্রের চিত্র",
Name: "বটের নাম", Name: "চরিত্রের নাম",
Sync: { Sync: {
Title: "গ্লোবাল কনফিগ ব্যবহার করুন", Title: "গ্লোবাল সেটিংস ব্যবহার করুন",
SubTitle: "এই চ্যাটে গ্লোবাল কনফিগ ব্যবহার করুন", SubTitle: "বর্তমান চ্যাট গ্লোবাল মডেল সেটিংস ব্যবহার করছে কি না",
Confirm: Confirm:
"গ্লোবাল কনফিগ দ্বারা কাস্টম কনফিগ ওভাররাইড করতে নিশ্চিত করতে?", "বর্তমান চ্যাটের কাস্টম সেটিংস স্বয়ংক্রিয়ভাবে ওভাররাইট হবে, গ্লোবাল সেটিংস সক্রিয় করতে নিশ্চিত?",
}, },
HideContext: { HideContext: {
Title: "সংশ্লিষ্টতা প্রম্পটগুলি লুকান", Title: "পূর্বনির্ধারিত চ্যাট লুকান",
SubTitle: "চ্যাটে সংশ্লিষ্টতা প্রম্পটগুলি দেখাবেন না", SubTitle:
"লুকানোর পরে পূর্বনির্ধারিত চ্যাট চ্যাট ইন্টারফেসে প্রদর্শিত হবে না",
}, },
Share: { Share: {
Title: "এই মাস্কটি শেয়ার করুন", Title: "এই মাস্ক শেয়ার করুন",
SubTitle: "এই মাস্কের একটি লিঙ্ক তৈরি করুন", SubTitle: "এই মাস্কের সরাসরি লিঙ্ক তৈরি করুন",
Action: "লিঙ্ক কপি করুন", Action: "লিঙ্ক কপি করুন",
}, },
}, },
}, },
NewChat: { NewChat: {
Return: "ফিরে যান", Return: "ফিরে যান",
Skip: "শুরু করুন", Skip: "ডাইরেক্ট শুরু করুন",
Title: "মাস্ক নির্বাচন করুন", NotShow: "আবার প্রদর্শন করবেন না",
SubTitle: "মাস্কের পিছনে আত্মার সঙ্গে চ্যাট করুন",
More: "আরো খুঁজুন",
NotShow: "এখনও দেখাবেন না",
ConfirmNoShow: ConfirmNoShow:
"নিষ্ক্রিয় করতে নিশ্চিত করুন? পরে আপনি এটি সেটিংসে সক্ষম করতে পারবেন।", "নিশ্চিত যে নিষ্ক্রিয় করবেন? নিষ্ক্রিয় করার পরে সেটিংসে পুনরায় সক্রিয় করা যাবে।",
Title: "একটি মাস্ক নির্বাচন করুন",
SubTitle: "এখন শুরু করুন, মাস্কের পিছনের চিন্তা প্রতিক্রিয়া করুন",
More: "সব দেখুন",
},
URLCommand: {
Code: "লিঙ্কে অ্যাক্সেস কোড ইতিমধ্যে অন্তর্ভুক্ত রয়েছে, অটো পূরণ করতে চান?",
Settings:
"লিঙ্কে প্রাক-নির্ধারিত সেটিংস অন্তর্ভুক্ত রয়েছে, অটো পূরণ করতে চান?",
}, },
UI: { UI: {
Confirm: "নিশ্চিত করুন", Confirm: "নিশ্চিত করুন",
Cancel: "বাতিল করুন", Cancel: "বাতিল করুন",
Close: "বন্ধ করুন", Close: "বন্ধ করুন",
Create: "তৈরি করুন", Create: "নতুন তৈরি করুন",
Edit: "সম্পাদন করুন", Edit: "সম্পাদনা করুন",
Export: "রপ্তানি করুন",
Import: "আমদানি করুন",
Sync: "সিঙ্ক",
Config: "কনফিগারেশন",
}, },
Exporter: { Exporter: {
Description: {
Title: "শুধুমাত্র কনটেক্সট পরিষ্কার করার পরে বার্তাগুলি প্রদর্শিত হবে",
},
Model: "মডেল", Model: "মডেল",
Messages: "বার্তা", Messages: "বার্তা",
Topic: "টপিক", Topic: "থিম",
Time: "সময়", Time: "সময়",
}, },
}; };

View File

@ -1,5 +1,6 @@
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp; const isApp = !!getClientConfig()?.isApp;
@ -7,16 +8,26 @@ const cn = {
WIP: "该功能仍在开发中……", WIP: "该功能仍在开发中……",
Error: { Error: {
Unauthorized: isApp Unauthorized: isApp
? "检测到无效 API Key请前往[设置](/#/settings)页检查 API Key 是否配置正确。" ? `😆 对话遇到了一些问题,不用慌:
: "访问密码不正确或为空,请前往[登录](/#/auth)页输入正确的访问密码,或者在[设置](/#/settings)页填入你自己的 OpenAI API Key。", \\ 1 [ 🚀](${SAAS_CHAT_UTM_URL})
\\ 2 OpenAI [](/#/settings) `
: `😆 对话遇到了一些问题,不用慌:
\ 1 [ 🚀](${SAAS_CHAT_UTM_URL})
\ 2 使[](/#/auth)访 🔑
\ 3 OpenAI [](/#/settings)
`,
}, },
Auth: { Auth: {
Return: "返回",
Title: "需要密码", Title: "需要密码",
Tips: "管理员开启了密码验证,请在下方填入访问码", Tips: "管理员开启了密码验证,请在下方填入访问码",
SubTips: "或者输入你的 OpenAI 或 Google API 密钥", SubTips: "或者输入你的 OpenAI 或 Google AI 密钥",
Input: "在此处填写访问码", Input: "在此处填写访问码",
Confirm: "确认", Confirm: "确认",
Later: "稍后再说", Later: "稍后再说",
SaasTips: "配置太麻烦,想要立即使用",
TopTips:
"🥳 NextChat AI 首发优惠,立刻解锁 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`, ChatItemCount: (count: number) => `${count} 条对话`,
@ -42,6 +53,11 @@ const cn = {
PinToastAction: "查看", PinToastAction: "查看",
Delete: "删除", Delete: "删除",
Edit: "编辑", Edit: "编辑",
FullScreen: "全屏",
RefreshTitle: "刷新标题",
RefreshToast: "已发送刷新标题请求",
Speech: "朗读",
StopSpeech: "停止",
}, },
Commands: { Commands: {
new: "新建聊天", new: "新建聊天",
@ -49,6 +65,7 @@ const cn = {
next: "下一个聊天", next: "下一个聊天",
prev: "上一个聊天", prev: "上一个聊天",
clear: "清除上下文", clear: "清除上下文",
fork: "复制聊天",
del: "删除聊天", del: "删除聊天",
}, },
InputActions: { InputActions: {
@ -75,11 +92,21 @@ const cn = {
return inputHints + "/ 触发补全,: 触发命令"; return inputHints + "/ 触发补全,: 触发命令";
}, },
Send: "发送", Send: "发送",
StartSpeak: "说话",
StopSpeak: "停止",
Config: { Config: {
Reset: "清除记忆", Reset: "清除记忆",
SaveAs: "存为面具", SaveAs: "存为面具",
}, },
IsContext: "预设提示词", IsContext: "预设提示词",
ShortcutKey: {
Title: "键盘快捷方式",
newChat: "打开新聊天",
focusInput: "聚焦输入框",
copyLastMessage: "复制最后一个回复",
copyLastCode: "复制最后一个代码块",
showShortcutKey: "显示快捷方式",
},
}, },
Export: { Export: {
Title: "分享聊天记录", Title: "分享聊天记录",
@ -104,6 +131,10 @@ const cn = {
Toast: "正在生成截图", Toast: "正在生成截图",
Modal: "长按或右键保存图片", Modal: "长按或右键保存图片",
}, },
Artifacts: {
Title: "分享页面",
Error: "分享失败",
},
}, },
Select: { Select: {
Search: "搜索消息", Search: "搜索消息",
@ -128,6 +159,7 @@ const cn = {
Settings: { Settings: {
Title: "设置", Title: "设置",
SubTitle: "所有设置选项", SubTitle: "所有设置选项",
ShowPassword: "显示密码",
Danger: { Danger: {
Reset: { Reset: {
@ -152,6 +184,11 @@ const cn = {
Title: "字体大小", Title: "字体大小",
SubTitle: "聊天内容的字体大小", SubTitle: "聊天内容的字体大小",
}, },
FontFamily: {
Title: "聊天字体",
SubTitle: "聊天内容的字体,若置空则应用全局默认字体",
Placeholder: "字体名称",
},
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "注入系统级提示信息", Title: "注入系统级提示信息",
SubTitle: "强制给每次请求的消息列表开头添加一个模拟 ChatGPT 的系统提示", SubTitle: "强制给每次请求的消息列表开头添加一个模拟 ChatGPT 的系统提示",
@ -168,6 +205,8 @@ const cn = {
IsChecking: "正在检查更新...", IsChecking: "正在检查更新...",
FoundUpdate: (x: string) => `发现新版本:${x}`, FoundUpdate: (x: string) => `发现新版本:${x}`,
GoToUpdate: "前往更新", GoToUpdate: "前往更新",
Success: "更新成功!",
Failed: "更新失败",
}, },
SendKey: "发送键", SendKey: "发送键",
Theme: "主题", Theme: "主题",
@ -271,6 +310,13 @@ const cn = {
}, },
Access: { Access: {
SaasStart: {
Title: "使用 NextChat AI",
Label: "(性价比最高的方案)",
SubTitle:
"由 NextChat 官方维护, 零配置开箱即用,支持 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
ChatNow: "立刻对话",
},
AccessCode: { AccessCode: {
Title: "访问密码", Title: "访问密码",
SubTitle: "管理员已开启加密访问", SubTitle: "管理员已开启加密访问",
@ -334,7 +380,7 @@ const cn = {
ApiKey: { ApiKey: {
Title: "API 密钥", Title: "API 密钥",
SubTitle: "从 Google AI 获取您的 API 密钥", SubTitle: "从 Google AI 获取您的 API 密钥",
Placeholder: "输入您的 Google AI Studio API 密钥", Placeholder: "Google AI API KEY",
}, },
Endpoint: { Endpoint: {
@ -346,6 +392,10 @@ const cn = {
Title: "API 版本(仅适用于 gemini-pro", Title: "API 版本(仅适用于 gemini-pro",
SubTitle: "选择一个特定的 API 版本", SubTitle: "选择一个特定的 API 版本",
}, },
GoogleSafetySettings: {
Title: "Google 安全过滤级别",
SubTitle: "设置内容过滤级别",
},
}, },
Baidu: { Baidu: {
ApiKey: { ApiKey: {
@ -363,6 +413,22 @@ const cn = {
SubTitle: "不支持自定义前往.env配置", SubTitle: "不支持自定义前往.env配置",
}, },
}, },
Tencent: {
ApiKey: {
Title: "API Key",
SubTitle: "使用自定义腾讯云API Key",
Placeholder: "Tencent API Key",
},
SecretKey: {
Title: "Secret Key",
SubTitle: "使用自定义腾讯云Secret Key",
Placeholder: "Tencent Secret Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "不支持自定义前往.env配置",
},
},
ByteDance: { ByteDance: {
ApiKey: { ApiKey: {
Title: "接口密钥", Title: "接口密钥",
@ -385,6 +451,55 @@ const cn = {
SubTitle: "样例:", SubTitle: "样例:",
}, },
}, },
Moonshot: {
ApiKey: {
Title: "接口密钥",
SubTitle: "使用自定义月之暗面API Key",
Placeholder: "Moonshot API Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "样例:",
},
},
XAI: {
ApiKey: {
Title: "接口密钥",
SubTitle: "使用自定义XAI API Key",
Placeholder: "XAI API Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "样例:",
},
},
Stability: {
ApiKey: {
Title: "接口密钥",
SubTitle: "使用自定义 Stability API Key",
Placeholder: "Stability API Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "样例:",
},
},
Iflytek: {
ApiKey: {
Title: "ApiKey",
SubTitle: "从讯飞星火控制台获取的 APIKey",
Placeholder: "APIKey",
},
ApiSecret: {
Title: "ApiSecret",
SubTitle: "从讯飞星火控制台获取的 APISecret",
Placeholder: "APISecret",
},
Endpoint: {
Title: "接口地址",
SubTitle: "样例:",
},
},
CustomModel: { CustomModel: {
Title: "自定义模型名", Title: "自定义模型名",
SubTitle: "增加自定义模型可选项,使用英文逗号隔开", SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
@ -392,6 +507,10 @@ const cn = {
}, },
Model: "模型 (model)", Model: "模型 (model)",
CompressModel: {
Title: "对话摘要模型",
SubTitle: "用于压缩历史记录、生成对话标题的模型",
},
Temperature: { Temperature: {
Title: "随机性 (temperature)", Title: "随机性 (temperature)",
SubTitle: "值越大,回复越随机", SubTitle: "值越大,回复越随机",
@ -412,6 +531,26 @@ const cn = {
Title: "频率惩罚度 (frequency_penalty)", Title: "频率惩罚度 (frequency_penalty)",
SubTitle: "值越大,越有可能降低重复字词", SubTitle: "值越大,越有可能降低重复字词",
}, },
TTS: {
Enable: {
Title: "启用文本转语音",
SubTitle: "启用文本生成语音服务",
},
Autoplay: {
Title: "启用自动朗读",
SubTitle: "自动生成语音并播放,需先开启文本转语音开关",
},
Model: "模型",
Engine: "转换引擎",
Voice: {
Title: "声音",
SubTitle: "生成语音时使用的声音",
},
Speed: {
Title: "速度",
SubTitle: "生成语音的速度",
},
},
}, },
Store: { Store: {
DefaultTopic: "新的聊天", DefaultTopic: "新的聊天",
@ -426,8 +565,8 @@ const cn = {
}, },
}, },
Copy: { Copy: {
Success: "已写入剪板", Success: "已写入剪板",
Failed: "复制失败,请赋予剪板权限", Failed: "复制失败,请赋予剪板权限",
}, },
Download: { Download: {
Success: "内容已下载到您的目录。", Success: "内容已下载到您的目录。",
@ -440,12 +579,67 @@ const cn = {
Clear: "上下文已清除", Clear: "上下文已清除",
Revert: "恢复上下文", Revert: "恢复上下文",
}, },
Plugin: { Discovery: {
Name: "插件", Name: "发现",
}, },
FineTuned: { FineTuned: {
Sysmessage: "你是一个助手", Sysmessage: "你是一个助手",
}, },
SearchChat: {
Name: "搜索",
Page: {
Title: "搜索聊天记录",
Search: "输入搜索关键词",
NoResult: "没有找到结果",
NoData: "没有数据",
Loading: "加载中",
SubTitle: (count: number) => `搜索到 ${count} 条结果`,
},
Item: {
View: "查看",
},
},
Plugin: {
Name: "插件",
Page: {
Title: "插件",
SubTitle: (count: number) => `${count} 个插件`,
Search: "搜索插件",
Create: "新建",
Find: "您可以在Github上找到优秀的插件",
},
Item: {
Info: (count: number) => `${count} 方法`,
View: "查看",
Edit: "编辑",
Delete: "删除",
DeleteConfirm: "确认删除?",
},
Auth: {
None: "不需要授权",
Basic: "Basic",
Bearer: "Bearer",
Custom: "自定义",
CustomHeader: "自定义参数名称",
Token: "Token",
Proxy: "使用代理",
ProxyDescription: "使用代理解决 CORS 错误",
Location: "位置",
LocationHeader: "Header",
LocationQuery: "Query",
LocationBody: "Body",
},
EditModal: {
Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
Download: "下载",
Auth: "授权方式",
Content: "OpenAPI Schema",
Load: "从网页加载",
Method: "方法",
Error: "格式错误",
},
},
Mask: { Mask: {
Name: "面具", Name: "面具",
Page: { Page: {
@ -480,6 +674,14 @@ const cn = {
Title: "隐藏预设对话", Title: "隐藏预设对话",
SubTitle: "隐藏后预设对话不会出现在聊天界面", SubTitle: "隐藏后预设对话不会出现在聊天界面",
}, },
Artifacts: {
Title: "启用Artifacts",
SubTitle: "启用之后可以直接渲染HTML页面",
},
CodeFold: {
Title: "启用代码折叠",
SubTitle: "启用之后可以自动折叠/展开过长的代码块",
},
Share: { Share: {
Title: "分享此面具", Title: "分享此面具",
SubTitle: "生成此面具的直达链接", SubTitle: "生成此面具的直达链接",
@ -522,6 +724,61 @@ const cn = {
Topic: "主题", Topic: "主题",
Time: "时间", Time: "时间",
}, },
SdPanel: {
Prompt: "画面提示",
NegativePrompt: "否定提示",
PleaseInput: (name: string) => `请输入${name}`,
AspectRatio: "横纵比",
ImageStyle: "图像风格",
OutFormat: "输出格式",
AIModel: "AI模型",
ModelVersion: "模型版本",
Submit: "提交生成",
ParamIsRequired: (name: string) => `${name}不能为空`,
Styles: {
D3Model: "3D模型",
AnalogFilm: "模拟电影",
Anime: "动漫",
Cinematic: "电影风格",
ComicBook: "漫画书",
DigitalArt: "数字艺术",
Enhance: "增强",
FantasyArt: "幻想艺术",
Isometric: "等角",
LineArt: "线描",
LowPoly: "低多边形",
ModelingCompound: "建模材料",
NeonPunk: "霓虹朋克",
Origami: "折纸",
Photographic: "摄影",
PixelArt: "像素艺术",
TileTexture: "贴图",
},
},
Sd: {
SubTitle: (count: number) => `${count} 条绘画`,
Actions: {
Params: "查看参数",
Copy: "复制提示词",
Delete: "删除",
Retry: "重试",
ReturnHome: "返回首页",
History: "查看历史",
},
EmptyRecord: "暂无绘画记录",
Status: {
Name: "状态",
Success: "成功",
Error: "失败",
Wait: "等待中",
Running: "运行中",
},
Danger: {
Delete: "确认删除?",
},
GenerateParams: "生成参数",
Detail: "详情",
},
}; };
type DeepPartial<T> = T extends object type DeepPartial<T> = T extends object

View File

@ -1,233 +1,586 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index"; import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
const cs: PartialLocaleType = { const cs: PartialLocaleType = {
WIP: "V přípravě...", WIP: "V přípravě...",
Error: { Error: {
Unauthorized: Unauthorized: isApp
"Neoprávněný přístup, zadejte přístupový kód na [stránce](/#/auth) nastavení.", ? `😆 Rozhovor narazil na nějaké problémy, nebojte se:
\\ 1 Pokud chcete začít bez konfigurace, [klikněte sem pro okamžitý začátek chatu 🚀](${SAAS_CHAT_UTM_URL})
\\ 2 Pokud chcete využít své vlastní zdroje OpenAI, klikněte [sem](/#/settings) a upravte nastavení `
: `😆 Rozhovor narazil na nějaké problémy, nebojte se:
\ 1 Pokud chcete začít bez konfigurace, [klikněte sem pro okamžitý začátek chatu 🚀](${SAAS_CHAT_UTM_URL})
\ 2 Pokud používáte verzi soukromého nasazení, klikněte [sem](/#/auth) a zadejte přístupový klíč 🔑
\ 3 Pokud chcete využít své vlastní zdroje OpenAI, klikněte [sem](/#/settings) a upravte nastavení
`,
},
Auth: {
Title: "Potřebné heslo",
Tips: "Administrátor povolil ověření heslem, prosím zadejte přístupový kód níže",
SubTips: "nebo zadejte svůj OpenAI nebo Google API klíč",
Input: "Zadejte přístupový kód zde",
Confirm: "Potvrdit",
Later: "Později",
Return: "Návrat",
SaasTips: "Konfigurace je příliš složitá, chci okamžitě začít používat",
TopTips:
"🥳 Uvítací nabídka NextChat AI, okamžitě odemkněte OpenAI o1, GPT-4o, Claude-3.5 a nejnovější velké modely",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} zpráv`, ChatItemCount: (count: number) => `${count} konverzací`,
}, },
Chat: { Chat: {
SubTitle: (count: number) => `${count} zpráv s ChatGPT`, SubTitle: (count: number) => `Celkem ${count} konverzací`,
EditMessage: {
Title: "Upravit zprávy",
Topic: {
Title: "Téma konverzace",
SubTitle: "Změnit aktuální téma konverzace",
},
},
Actions: { Actions: {
ChatList: "Přejít na seznam chatů", ChatList: "Zobrazit seznam zpráv",
CompressedHistory: "Pokyn z komprimované paměti historie", CompressedHistory: "Zobrazit komprimovanou historii Prompt",
Export: "Exportovat všechny zprávy jako Markdown", Export: "Exportovat konverzace",
Copy: "Kopírovat", Copy: "Kopírovat",
Stop: "Zastavit", Stop: "Zastavit",
Retry: "Zopakovat", Retry: "Zkusit znovu",
Pin: "Připnout",
PinToastContent: "1 konverzace byla připnuta k přednastaveným promptům",
PinToastAction: "Zobrazit",
Delete: "Smazat", Delete: "Smazat",
Edit: "Upravit",
RefreshTitle: "Obnovit název",
RefreshToast: "Požadavek na obnovení názvu byl odeslán",
}, },
Rename: "Přejmenovat chat", Commands: {
Typing: "Píše...", new: "Nová konverzace",
newm: "Nová konverzace z masky",
next: "Další konverzace",
prev: "Předchozí konverzace",
clear: "Vymazat kontext",
del: "Smazat konverzaci",
},
InputActions: {
Stop: "Zastavit odpověď",
ToBottom: "Přejít na nejnovější",
Theme: {
auto: "Automatické téma",
light: "Světelný režim",
dark: "Tmavý režim",
},
Prompt: "Rychlé příkazy",
Masks: "Všechny masky",
Clear: "Vymazat konverzaci",
Settings: "Nastavení konverzace",
UploadImage: "Nahrát obrázek",
},
Rename: "Přejmenovat konverzaci",
Typing: "Píše se…",
Input: (submitKey: string) => { Input: (submitKey: string) => {
var inputHints = `${submitKey} pro odeslání`; var inputHints = `${submitKey} odeslat`;
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter pro řádkování"; inputHints += "Shift + Enter pro nový řádek";
} }
return inputHints + ", / pro vyhledávání pokynů"; return inputHints + "/ pro doplnění, : pro příkaz";
}, },
Send: "Odeslat", Send: "Odeslat",
Config: { Config: {
Reset: "Obnovit výchozí", Reset: "Vymazat paměť",
SaveAs: "Uložit jako Masku", SaveAs: "Uložit jako masku",
}, },
IsContext: "Přednastavené prompty",
}, },
Export: { Export: {
Title: "Všechny zprávy", Title: "Sdílet konverzace",
Copy: "Kopírovat vše", Copy: "Kopírovat vše",
Download: "Stáhnout", Download: "Stáhnout soubor",
MessageFromYou: "Zpráva od vás", Share: "Sdílet na ShareGPT",
MessageFromChatGPT: "Zpráva z ChatGPT", MessageFromYou: "Uživatel",
MessageFromChatGPT: "ChatGPT",
Format: {
Title: "Formát exportu",
SubTitle: "Lze exportovat jako Markdown text nebo PNG obrázek",
},
IncludeContext: {
Title: "Zahrnout kontext masky",
SubTitle: "Zobrazit kontext masky ve zprávách",
},
Steps: {
Select: "Vybrat",
Preview: "Náhled",
},
Image: {
Toast: "Generování screenshotu",
Modal: "Dlouhým stiskem nebo pravým tlačítkem myši uložte obrázek",
},
},
Select: {
Search: "Hledat zprávy",
All: "Vybrat vše",
Latest: "Několik posledních",
Clear: "Zrušit výběr",
}, },
Memory: { Memory: {
Title: "Pokyn z paměti", Title: "Historie shrnutí",
EmptyContent: "Zatím nic.", EmptyContent: "Obsah konverzace je příliš krátký, není třeba shrnovat",
Send: "Odeslat paměť", Send: "Automaticky komprimovat konverzace a odeslat jako kontext",
Copy: "Kopírovat paměť", Copy: "Kopírovat shrnutí",
Reset: "Obnovit relaci", Reset: "[nepoužívá se]",
ResetConfirm: ResetConfirm: "Opravdu chcete vymazat historii shrnutí?",
"Resetováním se vymaže historie aktuálních konverzací i paměť historie pokynů. Opravdu chcete provést obnovu?",
}, },
Home: { Home: {
NewChat: "Nový chat", NewChat: "Nová konverzace",
DeleteChat: "Potvrzujete smazání vybrané konverzace?", DeleteChat: "Opravdu chcete smazat vybranou konverzaci?",
DeleteToast: "Chat smazán", DeleteToast: "Konverzace byla smazána",
Revert: "Zvrátit", Revert: "Vrátit",
}, },
Settings: { Settings: {
Title: "Nastavení", Title: "Nastavení",
SubTitle: "Všechna nastavení", SubTitle: "Všechny možnosti nastavení",
Danger: {
Reset: {
Title: "Obnovit všechna nastavení",
SubTitle: "Obnovit všechny nastavení na výchozí hodnoty",
Action: "Okamžitě obnovit",
Confirm: "Opravdu chcete obnovit všechna nastavení?",
},
Clear: {
Title: "Smazat všechna data",
SubTitle: "Smazat všechny chaty a nastavení",
Action: "Okamžitě smazat",
Confirm: "Opravdu chcete smazat všechny chaty a nastavení?",
},
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // POZOR: pokud chcete přidat nový překlad, prosím, nechte tuto hodnotu jako `Language`
All: "Všechny jazyky", All: "Všechny jazyky",
}, },
Avatar: "Avatar", Avatar: "Profilový obrázek",
FontSize: { FontSize: {
Title: "Velikost písma", Title: "Velikost písma",
SubTitle: "Nastavení velikosti písma obsahu chatu", SubTitle: "Velikost písma pro obsah chatu",
},
FontFamily: {
Title: "Chatové Písmo",
SubTitle:
"Písmo obsahu chatu, ponechejte prázdné pro použití globálního výchozího písma",
Placeholder: "Název Písma",
}, },
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "Vložit systémové prompty", Title: "Vložit systémové výzvy",
SubTitle: SubTitle:
"Vynutit přidání simulovaného systémového promptu ChatGPT na začátek seznamu zpráv každého požadavku", "Automaticky přidat systémovou výzvu simulující ChatGPT na začátek seznamu zpráv pro každý požadavek",
}, },
InputTemplate: {
Title: "Předzpracování uživatelského vstupu",
SubTitle: "Nejnovější zpráva uživatele bude vyplněna do této šablony",
},
Update: { Update: {
Version: (x: string) => `Verze: ${x}`, Version: (x: string) => `Aktuální verze: ${x}`,
IsLatest: "Aktuální verze", IsLatest: "Jste na nejnovější verzi",
CheckUpdate: "Zkontrolovat aktualizace", CheckUpdate: "Zkontrolovat aktualizace",
IsChecking: "Kontrola aktualizace...", IsChecking: "Kontrola aktualizací...",
FoundUpdate: (x: string) => `Nalezena nová verze: ${x}`, FoundUpdate: (x: string) => `Nalezena nová verze: ${x}`,
GoToUpdate: "Aktualizovat", GoToUpdate: "Přejít na aktualizaci",
}, },
SendKey: "Odeslat klíč", SendKey: "Klávesa pro odeslání",
Theme: "Téma", Theme: "Téma",
TightBorder: "Těsné ohraničení", TightBorder: "Režim bez okrajů",
SendPreviewBubble: { SendPreviewBubble: {
Title: "Odesílat chatovací bublinu s náhledem", Title: "Náhledová bublina",
SubTitle: "Zobrazit v náhledu bubliny", SubTitle: "Náhled Markdown obsahu v náhledové bublině",
},
AutoGenerateTitle: {
Title: "Automatické generování názvu",
SubTitle: "Generovat vhodný název na základě obsahu konverzace",
},
Sync: {
CloudState: "Data na cloudu",
NotSyncYet: "Ještě nebylo synchronizováno",
Success: "Synchronizace úspěšná",
Fail: "Synchronizace selhala",
Config: {
Modal: {
Title: "Nastavení cloudové synchronizace",
Check: "Zkontrolovat dostupnost",
},
SyncType: {
Title: "Typ synchronizace",
SubTitle: "Vyberte oblíbený synchronizační server",
},
Proxy: {
Title: "Povolit proxy",
SubTitle:
"Při synchronizaci v prohlížeči musí být proxy povolena, aby se předešlo problémům s CORS",
},
ProxyUrl: {
Title: "Adresa proxy",
SubTitle: "Pouze pro interní proxy",
},
WebDav: {
Endpoint: "WebDAV adresa",
UserName: "Uživatelské jméno",
Password: "Heslo",
},
UpStash: {
Endpoint: "UpStash Redis REST URL",
UserName: "Název zálohy",
Password: "UpStash Redis REST Token",
},
},
LocalState: "Lokální data",
Overview: (overview: any) => {
return `${overview.chat} konverzací, ${overview.message} zpráv, ${overview.prompt} promptů, ${overview.mask} masek`;
},
ImportFailed: "Import selhal",
}, },
Mask: { Mask: {
Splash: { Splash: {
Title: "Úvodní obrazovka Masek", Title: "Úvodní stránka masky",
SubTitle: "Před zahájením nového chatu zobrazte úvodní obrazovku Masek", SubTitle: "Při zahájení nové konverzace zobrazit úvodní stránku masky",
},
Builtin: {
Title: "Skrýt vestavěné masky",
SubTitle: "Skrýt vestavěné masky v seznamu všech masek",
}, },
}, },
Prompt: { Prompt: {
Disable: { Disable: {
Title: "Deaktivovat automatické dokončování", Title: "Zakázat automatické doplňování promptů",
SubTitle: "Zadejte / pro spuštění automatického dokončování", SubTitle:
"Automatické doplňování se aktivuje zadáním / na začátku textového pole",
}, },
List: "Seznam pokynů", List: "Seznam vlastních promptů",
ListCount: (builtin: number, custom: number) => ListCount: (builtin: number, custom: number) =>
`${builtin} vestavěných, ${custom} uživatelských`, `Vestavěné ${builtin} položek, uživatelsky definované ${custom} položek`,
Edit: "Upravit", Edit: "Upravit",
Modal: { Modal: {
Title: "Seznam pokynů", Title: "Seznam promptů",
Add: "Přidat pokyn", Add: "Nový",
Search: "Hledat pokyny", Search: "Hledat prompty",
}, },
EditModal: { EditModal: {
Title: "Editovat pokyn", Title: "Upravit prompt",
}, },
}, },
HistoryCount: { HistoryCount: {
Title: "Počet připojených zpráv", Title: "Počet historických zpráv",
SubTitle: "Počet odeslaných připojených zpráv na žádost", SubTitle: "Počet historických zpráv zahrnutých v každém požadavku",
}, },
CompressThreshold: { CompressThreshold: {
Title: "Práh pro kompresi historie", Title: "Prahová hodnota komprese historických zpráv",
SubTitle: SubTitle:
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu", "Když nekomprimované historické zprávy překročí tuto hodnotu, dojde ke kompresi",
}, },
Usage: { Usage: {
Title: "Stav účtu", Title: "Kontrola zůstatku",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
return `Použito tento měsíc $${used}, předplaceno $${total}`; return `Tento měsíc použito $${used}, celkový předplatný objem $${total}`;
}, },
IsChecking: "Kontroluji...", IsChecking: "Probíhá kontrola…",
Check: "Zkontrolovat", Check: "Znovu zkontrolovat",
NoAccess: "Pro kontrolu zůstatku zadejte klíč API", NoAccess: "Zadejte API Key nebo přístupové heslo pro zobrazení zůstatku",
}, },
Model: "Model", Access: {
SaasStart: {
Title: "Použití NextChat AI",
Label: "(Nejlepší nákladově efektivní řešení)",
SubTitle:
"Oficiálně udržováno NextChat, připraveno k použití bez konfigurace, podporuje nejnovější velké modely jako OpenAI o1, GPT-4o, Claude-3.5",
ChatNow: "Začněte chatovat nyní",
},
AccessCode: {
Title: "Přístupový kód",
SubTitle: "Administrátor aktivoval šifrovaný přístup",
Placeholder: "Zadejte přístupový kód",
},
CustomEndpoint: {
Title: "Vlastní rozhraní",
SubTitle: "Použít vlastní Azure nebo OpenAI službu",
},
Provider: {
Title: "Poskytovatel modelu",
SubTitle: "Přepnout mezi různými poskytovateli",
},
OpenAI: {
ApiKey: {
Title: "API Key",
SubTitle:
"Použijte vlastní OpenAI Key k obejití přístupového omezení",
Placeholder: "OpenAI API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Kromě výchozí adresy musí obsahovat http(s)://",
},
},
Azure: {
ApiKey: {
Title: "Rozhraní klíč",
SubTitle: "Použijte vlastní Azure Key k obejití přístupového omezení",
Placeholder: "Azure API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Příklad:",
},
ApiVerion: {
Title: "Verze rozhraní (azure api version)",
SubTitle: "Vyberte konkrétní verzi",
},
},
Anthropic: {
ApiKey: {
Title: "Rozhraní klíč",
SubTitle:
"Použijte vlastní Anthropic Key k obejití přístupového omezení",
Placeholder: "Anthropic API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Příklad:",
},
ApiVerion: {
Title: "Verze rozhraní (claude api version)",
SubTitle: "Vyberte konkrétní verzi API",
},
},
Google: {
ApiKey: {
Title: "API klíč",
SubTitle: "Získejte svůj API klíč od Google AI",
Placeholder: "Zadejte svůj Google AI Studio API klíč",
},
Endpoint: {
Title: "Konečná adresa",
SubTitle: "Příklad:",
},
ApiVersion: {
Title: "Verze API (pouze pro gemini-pro)",
SubTitle: "Vyberte konkrétní verzi API",
},
GoogleSafetySettings: {
Title: "Úroveň bezpečnostního filtrování Google",
SubTitle: "Nastavit úroveň filtrování obsahu",
},
},
Baidu: {
ApiKey: {
Title: "API Key",
SubTitle: "Použijte vlastní Baidu API Key",
Placeholder: "Baidu API Key",
},
SecretKey: {
Title: "Secret Key",
SubTitle: "Použijte vlastní Baidu Secret Key",
Placeholder: "Baidu Secret Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle:
"Nepodporuje vlastní nastavení, přejděte na .env konfiguraci",
},
},
ByteDance: {
ApiKey: {
Title: "Rozhraní klíč",
SubTitle: "Použijte vlastní ByteDance API Key",
Placeholder: "ByteDance API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Příklad:",
},
},
Alibaba: {
ApiKey: {
Title: "Rozhraní klíč",
SubTitle: "Použijte vlastní Alibaba Cloud API Key",
Placeholder: "Alibaba Cloud API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Příklad:",
},
},
CustomModel: {
Title: "Vlastní názvy modelů",
SubTitle: "Přidejte možnosti vlastních modelů, oddělené čárkami",
},
},
Model: "Model (model)",
CompressModel: {
Title: "Kompresní model",
SubTitle: "Model používaný pro kompresi historie",
},
Temperature: { Temperature: {
Title: "Teplota", Title: "Náhodnost (temperature)",
SubTitle: "Větší hodnota činí výstup náhodnějším", SubTitle: "Čím vyšší hodnota, tím náhodnější odpovědi",
},
TopP: {
Title: "Jádrové vzorkování (top_p)",
SubTitle: "Podobné náhodnosti, ale neměňte spolu s náhodností",
}, },
MaxTokens: { MaxTokens: {
Title: "Max. počet tokenů", Title: "Omezení odpovědi (max_tokens)",
SubTitle: "Maximální délka vstupního tokenu a generovaných tokenů", SubTitle: "Maximální počet Tokenů použitých v jednom interakci",
}, },
PresencePenalty: { PresencePenalty: {
Title: "Přítomnostní korekce", Title: "Čerstvost témat (presence_penalty)",
SubTitle: "Větší hodnota zvyšuje pravděpodobnost nových témat.", SubTitle:
"Čím vyšší hodnota, tím větší pravděpodobnost rozšíření na nová témata",
}, },
FrequencyPenalty: { FrequencyPenalty: {
Title: "Frekvenční penalizace", Title: "Penalizace frekvence (frequency_penalty)",
SubTitle: SubTitle:
"Větší hodnota snižující pravděpodobnost opakování stejného řádku", "Čím vyšší hodnota, tím větší pravděpodobnost snížení opakování slov",
}, },
}, },
Store: { Store: {
DefaultTopic: "Nová konverzace", DefaultTopic: "Nový chat",
BotHello: "Ahoj! Jak mohu dnes pomoci?", BotHello: "Jak vám mohu pomoci?",
Error: "Něco se pokazilo, zkuste to prosím později.", Error: "Došlo k chybě, zkuste to prosím znovu později.",
Prompt: { Prompt: {
History: (content: string) => History: (content: string) =>
"Toto je shrnutí historie chatu mezi umělou inteligencí a uživatelem v podobě rekapitulace: " + "Toto je shrnutí historie chatu jako kontext: " + content,
content,
Topic: Topic:
"Vytvořte prosím název o čtyřech až pěti slovech vystihující průběh našeho rozhovoru bez jakýchkoli úvodních slov, interpunkčních znamének, uvozovek, teček, symbolů nebo dalšího textu. Odstraňte uvozovky.", "Použijte čtyři až pět slov pro stručné téma této věty, bez vysvětlení, interpunkce, citoslovcí, nadbytečného textu, bez tučného písma. Pokud téma neexistuje, vraťte pouze 'neformální chat'.",
Summarize: Summarize:
"Krátce shrň naši diskusi v rozsahu do 200 slov a použij ji jako podnět pro budoucí kontext.", "Stručně shrňte obsah konverzace jako kontextový prompt pro budoucí použití, do 200 slov",
}, },
}, },
Copy: { Copy: {
Success: "Zkopírováno do schránky", Success: "Zkopírováno do schránky",
Failed: "Kopírování selhalo, prosím, povolte přístup ke schránce", Failed: "Kopírování selhalo, prosím, povolte přístup ke schránce",
}, },
Download: {
Success: "Obsah byl stažen do vašeho adresáře.",
Failed: "Stahování selhalo.",
},
Context: { Context: {
Toast: (x: any) => `Použití ${x} kontextových pokynů`, Toast: (x: any) => `Obsahuje ${x} přednastavených promptů`,
Edit: "Kontextové a paměťové pokyny", Edit: "Nastavení aktuální konverzace",
Add: "Přidat pokyn", Add: "Přidat novou konverzaci",
Clear: "Kontext byl vymazán",
Revert: "Obnovit kontext",
}, },
Plugin: { Plugin: {
Name: "Plugin", Name: "Plugin",
}, },
FineTuned: { FineTuned: {
Sysmessage: "Jste asistent, který", Sysmessage: "Jste asistent",
},
SearchChat: {
Name: "Hledat",
Page: {
Title: "Hledat v historii chatu",
Search: "Zadejte hledané klíčové slovo",
NoResult: "Nebyly nalezeny žádné výsledky",
NoData: "Žádná data",
Loading: "Načítání",
SubTitle: (count: number) => `Nalezeno ${count} výsledků`,
},
Item: {
View: "Zobrazit",
},
}, },
Mask: { Mask: {
Name: "Maska", Name: "Maska",
Page: { Page: {
Title: "Šablona pokynu", Title: "Přednastavené role masky",
SubTitle: (count: number) => `${count} šablon pokynů`, SubTitle: (count: number) => `${count} definovaných rolí`,
Search: "Hledat v šablonách", Search: "Hledat role masky",
Create: "Vytvořit", Create: "Nový",
}, },
Item: { Item: {
Info: (count: number) => `${count} pokynů`, Info: (count: number) => `Obsahuje ${count} přednastavených konverzací`,
Chat: "Chat", Chat: "Chat",
View: "Zobrazit", View: "Zobrazit",
Edit: "Upravit", Edit: "Upravit",
Delete: "Smazat", Delete: "Smazat",
DeleteConfirm: "Potvrdit smazání?", DeleteConfirm: "Opravdu chcete smazat?",
}, },
EditModal: { EditModal: {
Title: (readonly: boolean) => Title: (readonly: boolean) =>
`Editovat šablonu pokynu ${readonly ? "(pouze ke čtení)" : ""}`, `Upravit přednastavenou masku ${readonly ? " (jen pro čtení)" : ""}`,
Download: "Stáhnout", Download: "Stáhnout přednastavení",
Clone: "Duplikovat", Clone: "Klonovat přednastavení",
}, },
Config: { Config: {
Avatar: "Avatar Bota", Avatar: "Profilový obrázek",
Name: "Jméno Bota", Name: "Název role",
Sync: {
Title: "Použít globální nastavení",
SubTitle: "Použít globální modelová nastavení pro aktuální konverzaci",
Confirm:
"Vaše vlastní nastavení konverzace bude automaticky přepsáno, opravdu chcete použít globální nastavení?",
},
HideContext: {
Title: "Skrýt přednastavené konverzace",
SubTitle:
"Po skrytí se přednastavené konverzace nebudou zobrazovat v chatovém rozhraní",
},
Share: {
Title: "Sdílet tuto masku",
SubTitle: "Generovat přímý odkaz na tuto masku",
Action: "Kopírovat odkaz",
},
}, },
}, },
NewChat: { NewChat: {
Return: "Zpět", Return: "Zpět",
Skip: "Přeskočit", Skip: "Začít hned",
Title: "Vyberte Masku", NotShow: "Zobrazit už nikdy",
SubTitle: "Chatovat s duší za Maskou", ConfirmNoShow:
More: "Najít více", "Opravdu chcete zakázat? Zakázání můžete kdykoli znovu povolit v nastavení.",
NotShow: "Nezobrazovat znovu", Title: "Vyberte masku",
ConfirmNoShow: "Potvrdit zakázáníMůžete jej povolit později v nastavení.", SubTitle: "Začněte nyní a konfrontujte se s myslí za maskou",
More: "Zobrazit vše",
},
URLCommand: {
Code: "Byl detekován přístupový kód v odkazu, chcete jej automaticky vyplnit?",
Settings:
"Byla detekována přednastavená nastavení v odkazu, chcete je automaticky vyplnit?",
}, },
UI: { UI: {
Confirm: "Potvrdit", Confirm: "Potvrdit",
Cancel: "Zrušit", Cancel: "Zrušit",
Close: "Zavřít", Close: "Zavřít",
Create: "Vytvořit", Create: "Nový",
Edit: "Upravit", Edit: "Upravit",
Export: "Exportovat",
Import: "Importovat",
Sync: "Synchronizovat",
Config: "Konfigurovat",
}, },
Exporter: { Exporter: {
Description: {
Title: "Pouze zprávy po vymazání kontextu budou zobrazeny",
},
Model: "Model", Model: "Model",
Messages: "Zprávy", Messages: "Zprávy",
Topic: "Téma", Topic: "Téma",

View File

@ -1,235 +1,605 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index"; import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
const de: PartialLocaleType = { const de: PartialLocaleType = {
WIP: "In Bearbeitung...", WIP: "In Bearbeitung...",
Error: { Error: {
Unauthorized: Unauthorized: isApp
"Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der [Einstellungsseite](/#/auth) ein.", ? `😆 Das Gespräch hatte einige Probleme, keine Sorge:
\\ 1 Wenn du ohne Konfiguration sofort starten möchtest, [klicke hier, um sofort zu chatten 🚀](${SAAS_CHAT_UTM_URL})
\\ 2 Wenn du deine eigenen OpenAI-Ressourcen verwenden möchtest, klicke [hier](/#/settings), um die Einstellungen zu ändern `
: `😆 Das Gespräch hatte einige Probleme, keine Sorge:
\ 1 Wenn du ohne Konfiguration sofort starten möchtest, [klicke hier, um sofort zu chatten 🚀](${SAAS_CHAT_UTM_URL})
\ 2 Wenn du eine private Bereitstellung verwendest, klicke [hier](/#/auth), um den Zugriffsschlüssel einzugeben 🔑
\ 3 Wenn du deine eigenen OpenAI-Ressourcen verwenden möchtest, klicke [hier](/#/settings), um die Einstellungen zu ändern
`,
},
Auth: {
Title: "Passwort erforderlich",
Tips: "Der Administrator hat die Passwortüberprüfung aktiviert. Bitte geben Sie den Zugangscode unten ein.",
SubTips: "Oder geben Sie Ihren OpenAI oder Google API-Schlüssel ein.",
Input: "Geben Sie hier den Zugangscode ein",
Confirm: "Bestätigen",
Later: "Später",
Return: "Zurück",
SaasTips:
"Die Konfiguration ist zu kompliziert, ich möchte es sofort nutzen",
TopTips:
"🥳 NextChat AI Einführungsangebot, schalte jetzt OpenAI o1, GPT-4o, Claude-3.5 und die neuesten großen Modelle frei",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} Nachrichten`, ChatItemCount: (count: number) => `${count} Gespräche`,
}, },
Chat: { Chat: {
SubTitle: (count: number) => `${count} Nachrichten mit ChatGPT`, SubTitle: (count: number) => `Insgesamt ${count} Gespräche`,
Actions: { EditMessage: {
ChatList: "Zur Chat-Liste gehen", Title: "Nachricht bearbeiten",
CompressedHistory: "Komprimierter Gedächtnis-Prompt", Topic: {
Export: "Alle Nachrichten als Markdown exportieren", Title: "Chat-Thema",
Copy: "Kopieren", SubTitle: "Ändern Sie das aktuelle Chat-Thema",
Stop: "Stop", },
Retry: "Wiederholen",
Delete: "Delete",
}, },
Rename: "Chat umbenennen", Actions: {
Typing: "Tippen...", ChatList: "Nachrichtliste anzeigen",
CompressedHistory: "Komprimierte Historie anzeigen",
Export: "Chatverlauf exportieren",
Copy: "Kopieren",
Stop: "Stoppen",
Retry: "Erneut versuchen",
Pin: "Anheften",
PinToastContent: "1 Gespräch an den voreingestellten Prompt angeheftet",
PinToastAction: "Ansehen",
Delete: "Löschen",
Edit: "Bearbeiten",
RefreshTitle: "Titel aktualisieren",
RefreshToast: "Anfrage zur Titelaktualisierung gesendet",
},
Commands: {
new: "Neues Gespräch",
newm: "Neues Gespräch aus Maske erstellen",
next: "Nächstes Gespräch",
prev: "Vorheriges Gespräch",
clear: "Kontext löschen",
del: "Gespräch löschen",
},
InputActions: {
Stop: "Antwort stoppen",
ToBottom: "Zum neuesten Beitrag",
Theme: {
auto: "Automatisches Thema",
light: "Helles Thema",
dark: "Dunkles Thema",
},
Prompt: "Schnellbefehle",
Masks: "Alle Masken",
Clear: "Chat löschen",
Settings: "Gesprächseinstellungen",
UploadImage: "Bild hochladen",
},
Rename: "Gespräch umbenennen",
Typing: "Tippt…",
Input: (submitKey: string) => { Input: (submitKey: string) => {
var inputHints = `${submitKey} um zu Senden`; var inputHints = `${submitKey} senden`;
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Umschalt + Eingabe für Zeilenumbruch"; inputHints += "Shift + Enter für Zeilenumbruch";
} }
return inputHints + ", / zum Durchsuchen von Prompts"; return inputHints + "/ für Autovervollständigung, : für Befehle";
}, },
Send: "Senden", Send: "Senden",
Config: { Config: {
Reset: "Reset to Default", Reset: "Erinnerung löschen",
SaveAs: "Save as Mask", SaveAs: "Als Maske speichern",
}, },
IsContext: "Voreingestellter Prompt",
}, },
Export: { Export: {
Title: "Alle Nachrichten", Title: "Chatverlauf teilen",
Copy: "Alles kopieren", Copy: "Alles kopieren",
Download: "Herunterladen", Download: "Datei herunterladen",
MessageFromYou: "Deine Nachricht", Share: "Auf ShareGPT teilen",
MessageFromChatGPT: "Nachricht von ChatGPT", MessageFromYou: "Benutzer",
MessageFromChatGPT: "ChatGPT",
Format: {
Title: "Exportformat",
SubTitle: "Kann als Markdown-Text oder PNG-Bild exportiert werden",
},
IncludeContext: {
Title: "Maske Kontext einbeziehen",
SubTitle: "Soll der Maskenkontext in den Nachrichten angezeigt werden?",
},
Steps: {
Select: "Auswählen",
Preview: "Vorschau",
},
Image: {
Toast: "Screenshot wird erstellt",
Modal: "Lang drücken oder Rechtsklick, um Bild zu speichern",
},
},
Select: {
Search: "Nachrichten suchen",
All: "Alles auswählen",
Latest: "Neueste",
Clear: "Auswahl aufheben",
}, },
Memory: { Memory: {
Title: "Gedächtnis-Prompt", Title: "Historische Zusammenfassung",
EmptyContent: "Noch nichts.", EmptyContent:
Send: "Gedächtnis senden", "Gesprächsinhalte sind zu kurz, keine Zusammenfassung erforderlich",
Copy: "Gedächtnis kopieren", Send: "Chatverlauf automatisch komprimieren und als Kontext senden",
Reset: "Sitzung zurücksetzen", Copy: "Zusammenfassung kopieren",
ResetConfirm: Reset: "[nicht verwendet]",
"Das Zurücksetzen löscht den aktuellen Gesprächsverlauf und das Langzeit-Gedächtnis. Möchten Sie wirklich zurücksetzen?", ResetConfirm: "Zusammenfassung löschen bestätigen?",
}, },
Home: { Home: {
NewChat: "Neuer Chat", NewChat: "Neues Gespräch",
DeleteChat: "Bestätigen Sie, um das ausgewählte Gespräch zu löschen?", DeleteChat: "Bestätigen Sie das Löschen des ausgewählten Gesprächs?",
DeleteToast: "Chat gelöscht", DeleteToast: "Gespräch gelöscht",
Revert: "Zurücksetzen", Revert: "Rückgängig machen",
}, },
Settings: { Settings: {
Title: "Einstellungen", Title: "Einstellungen",
SubTitle: "Alle Einstellungen", SubTitle: "Alle Einstellungsmöglichkeiten",
Danger: {
Reset: {
Title: "Alle Einstellungen zurücksetzen",
SubTitle: "Setzt alle Einstellungen auf die Standardwerte zurück",
Action: "Jetzt zurücksetzen",
Confirm: "Bestätigen Sie das Zurücksetzen aller Einstellungen?",
},
Clear: {
Title: "Alle Daten löschen",
SubTitle: "Löscht alle Chats und Einstellungsdaten",
Action: "Jetzt löschen",
Confirm:
"Bestätigen Sie das Löschen aller Chats und Einstellungsdaten?",
},
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Sprache", // ACHTUNG: Wenn Sie eine neue Übersetzung hinzufügen möchten, übersetzen Sie diesen Wert bitte nicht, lassen Sie ihn als `Sprache`
All: "Alle Sprachen", All: "Alle Sprachen",
}, },
Avatar: "Avatar", Avatar: "Avatar",
FontSize: { FontSize: {
Title: "Schriftgröße", Title: "Schriftgröße",
SubTitle: "Schriftgröße des Chat-Inhalts anpassen", SubTitle: "Schriftgröße des Chat-Inhalts",
},
FontFamily: {
Title: "Chat-Schriftart",
SubTitle:
"Schriftart des Chat-Inhalts, leer lassen, um die globale Standardschriftart anzuwenden",
Placeholder: "Schriftartname",
}, },
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "System-Prompts einfügen", Title: "Systemweite Eingabeaufforderungen einfügen",
SubTitle: SubTitle:
"Erzwingt das Hinzufügen eines simulierten systemweiten Prompts von ChatGPT am Anfang der Nachrichtenliste bei jeder Anfrage", "Fügt jeder Nachricht am Anfang der Nachrichtenliste eine simulierte ChatGPT-Systemaufforderung hinzu",
}, },
InputTemplate: {
Title: "Benutzer-Eingabeverarbeitung",
SubTitle:
"Die neueste Nachricht des Benutzers wird in diese Vorlage eingefügt",
},
Update: { Update: {
Version: (x: string) => `Version: ${x}`, Version: (x: string) => `Aktuelle Version: ${x}`,
IsLatest: "Neueste Version", IsLatest: "Bereits die neueste Version",
CheckUpdate: "Update prüfen", CheckUpdate: "Auf Updates überprüfen",
IsChecking: "Update wird geprüft...", IsChecking: "Überprüfe auf Updates...",
FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`, FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`,
GoToUpdate: "Aktualisieren", GoToUpdate: "Zum Update gehen",
}, },
SendKey: "Senden-Taste", SendKey: "Sende-Taste",
Theme: "Erscheinungsbild", Theme: "Thema",
TightBorder: "Enger Rahmen", TightBorder: "Randloser Modus",
SendPreviewBubble: { SendPreviewBubble: {
Title: "Vorschau-Bubble senden", Title: "Vorschau-Bubble",
SubTitle: "Preview markdown in bubble", SubTitle: "Markdown-Inhalt in der Vorschau-Bubble anzeigen",
},
AutoGenerateTitle: {
Title: "Titel automatisch generieren",
SubTitle:
"Basierend auf dem Chat-Inhalt einen passenden Titel generieren",
},
Sync: {
CloudState: "Cloud-Daten",
NotSyncYet: "Noch nicht synchronisiert",
Success: "Synchronisation erfolgreich",
Fail: "Synchronisation fehlgeschlagen",
Config: {
Modal: {
Title: "Cloud-Synchronisation konfigurieren",
Check: "Verfügbarkeit überprüfen",
},
SyncType: {
Title: "Synchronisationstyp",
SubTitle: "Wählen Sie den bevorzugten Synchronisationsserver aus",
},
Proxy: {
Title: "Proxy aktivieren",
SubTitle:
"Beim Synchronisieren im Browser muss ein Proxy aktiviert werden, um Cross-Origin-Beschränkungen zu vermeiden",
},
ProxyUrl: {
Title: "Proxy-Adresse",
SubTitle: "Nur für projektinterne Cross-Origin-Proxy",
},
WebDav: {
Endpoint: "WebDAV-Adresse",
UserName: "Benutzername",
Password: "Passwort",
},
UpStash: {
Endpoint: "UpStash Redis REST-Url",
UserName: "Sicherungsname",
Password: "UpStash Redis REST-Token",
},
},
LocalState: "Lokale Daten",
Overview: (overview: any) => {
return `${overview.chat} Chats, ${overview.message} Nachrichten, ${overview.prompt} Eingabeaufforderungen, ${overview.mask} Masken`;
},
ImportFailed: "Import fehlgeschlagen",
}, },
Mask: { Mask: {
Splash: { Splash: {
Title: "Mask Splash Screen", Title: "Masken-Startseite",
SubTitle: "Show a mask splash screen before starting new chat", SubTitle:
"Zeige die Masken-Startseite beim Erstellen eines neuen Chats",
},
Builtin: {
Title: "Eingebaute Masken ausblenden",
SubTitle: "Blendet eingebaute Masken in allen Maskenlisten aus",
}, },
}, },
Prompt: { Prompt: {
Disable: { Disable: {
Title: "Autovervollständigung deaktivieren", Title: "Automatische Eingabeaufforderung deaktivieren",
SubTitle: "Autovervollständigung mit / starten", SubTitle:
"Geben Sie am Anfang des Eingabefelds / ein, um die automatische Vervollständigung auszulösen",
}, },
List: "Prompt-Liste", List: "Benutzerdefinierte Eingabeaufforderungsliste",
ListCount: (builtin: number, custom: number) => ListCount: (builtin: number, custom: number) =>
`${builtin} integriert, ${custom} benutzerdefiniert`, `Eingebaut ${builtin} Stück, Benutzerdefiniert ${custom} Stück`,
Edit: "Bearbeiten", Edit: "Bearbeiten",
Modal: { Modal: {
Title: "Prompt List", Title: "Eingabeaufforderungsliste",
Add: "Add One", Add: "Neu erstellen",
Search: "Search Prompts", Search: "Eingabeaufforderungen suchen",
}, },
EditModal: { EditModal: {
Title: "Edit Prompt", Title: "Eingabeaufforderung bearbeiten",
}, },
}, },
HistoryCount: { HistoryCount: {
Title: "Anzahl der angehängten Nachrichten", Title: "Anzahl der historischen Nachrichten",
SubTitle: "Anzahl der pro Anfrage angehängten gesendeten Nachrichten", SubTitle:
"Anzahl der historischen Nachrichten, die bei jeder Anfrage mitgesendet werden",
}, },
CompressThreshold: { CompressThreshold: {
Title: "Schwellenwert für Verlaufskomprimierung", Title: "Komprimierungsschwelle für historische Nachrichtenlänge",
SubTitle: SubTitle:
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet", "Wenn die unkomprimierten historischen Nachrichten diesen Wert überschreiten, wird komprimiert",
}, },
Usage: { Usage: {
Title: "Kontostand", Title: "Guthabenabfrage",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
return `Diesen Monat ausgegeben $${used}, Abonnement $${total}`; return `In diesem Monat verwendet $${used}, Abonnement insgesamt $${total}`;
}, },
IsChecking: "Wird überprüft...", IsChecking: "Wird überprüft…",
Check: "Erneut prüfen", Check: "Erneut überprüfen",
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen", NoAccess:
"Geben Sie API-Schlüssel oder Zugangspasswort ein, um das Guthaben einzusehen",
}, },
Access: {
SaasStart: {
Title: "NextChat AI verwenden",
Label: "(Die kosteneffektivste Lösung)",
SubTitle:
"Offiziell von NextChat verwaltet, sofort einsatzbereit ohne Konfiguration, unterstützt die neuesten großen Modelle wie OpenAI o1, GPT-4o und Claude-3.5",
ChatNow: "Jetzt chatten",
},
AccessCode: {
Title: "Zugangscode",
SubTitle:
"Der Administrator hat die verschlüsselte Zugriffskontrolle aktiviert",
Placeholder: "Geben Sie den Zugangscode ein",
},
CustomEndpoint: {
Title: "Benutzerdefinierte Schnittstelle",
SubTitle: "Benutzerdefinierte Azure- oder OpenAI-Dienste verwenden",
},
Provider: {
Title: "Modellanbieter",
SubTitle: "Wechseln Sie zu verschiedenen Anbietern",
},
OpenAI: {
ApiKey: {
Title: "API-Schlüssel",
SubTitle:
"Verwenden Sie benutzerdefinierten OpenAI-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen",
Placeholder: "OpenAI API-Schlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle: "Neben der Standardadresse muss http(s):// enthalten sein",
},
},
Azure: {
ApiKey: {
Title: "Schnittstellenschlüssel",
SubTitle:
"Verwenden Sie benutzerdefinierten Azure-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen",
Placeholder: "Azure API-Schlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle: "Beispiel:",
},
ApiVerion: {
Title: "Schnittstellenversion (azure api version)",
SubTitle: "Wählen Sie eine spezifische Teilversion aus",
},
},
Anthropic: {
ApiKey: {
Title: "Schnittstellenschlüssel",
SubTitle:
"Verwenden Sie benutzerdefinierten Anthropic-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen",
Placeholder: "Anthropic API-Schlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle: "Beispiel:",
},
ApiVerion: {
Title: "Schnittstellenversion (claude api version)",
SubTitle: "Wählen Sie eine spezifische API-Version aus",
},
},
Google: {
ApiKey: {
Title: "API-Schlüssel",
SubTitle: "Holen Sie sich Ihren API-Schlüssel von Google AI",
Placeholder: "Geben Sie Ihren Google AI Studio API-Schlüssel ein",
},
Endpoint: {
Title: "Endpunktadresse",
SubTitle: "Beispiel:",
},
ApiVersion: {
Title: "API-Version (nur für gemini-pro)",
SubTitle: "Wählen Sie eine spezifische API-Version aus",
},
GoogleSafetySettings: {
Title: "Google Sicherheitsfilterstufe",
SubTitle: "Inhaltfilterstufe einstellen",
},
},
Baidu: {
ApiKey: {
Title: "API-Schlüssel",
SubTitle: "Verwenden Sie benutzerdefinierten Baidu API-Schlüssel",
Placeholder: "Baidu API-Schlüssel",
},
SecretKey: {
Title: "Geheimschlüssel",
SubTitle: "Verwenden Sie benutzerdefinierten Baidu Geheimschlüssel",
Placeholder: "Baidu Geheimschlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle:
"Keine benutzerdefinierten Adressen unterstützen, konfigurieren Sie in .env",
},
},
ByteDance: {
ApiKey: {
Title: "Schnittstellenschlüssel",
SubTitle: "Verwenden Sie benutzerdefinierten ByteDance API-Schlüssel",
Placeholder: "ByteDance API-Schlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle: "Beispiel:",
},
},
Alibaba: {
ApiKey: {
Title: "Schnittstellenschlüssel",
SubTitle:
"Verwenden Sie benutzerdefinierten Alibaba Cloud API-Schlüssel",
Placeholder: "Alibaba Cloud API-Schlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle: "Beispiel:",
},
},
CustomModel: {
Title: "Benutzerdefinierter Modellname",
SubTitle:
"Fügen Sie benutzerdefinierte Modelloptionen hinzu, getrennt durch Kommas",
},
},
Model: "Modell", Model: "Modell",
CompressModel: {
Title: "Kompressionsmodell",
SubTitle: "Modell zur Komprimierung des Verlaufs",
},
Temperature: { Temperature: {
Title: "Temperature", //Temperatur Title: "Zufälligkeit (temperature)",
SubTitle: "Ein größerer Wert führt zu zufälligeren Antworten", SubTitle: "Je höher der Wert, desto zufälliger die Antwort",
},
TopP: {
Title: "Kern-Sampling (top_p)",
SubTitle:
"Ähnlich der Zufälligkeit, aber nicht zusammen mit Zufälligkeit ändern",
}, },
MaxTokens: { MaxTokens: {
Title: "Max Tokens", //Maximale Token Title: "Maximale Token-Anzahl pro Antwort",
SubTitle: "Maximale Anzahl der Anfrage- plus Antwort-Token", SubTitle: "Maximale Anzahl der Tokens pro Interaktion",
}, },
PresencePenalty: { PresencePenalty: {
Title: "Presence Penalty", //Anwesenheitsstrafe Title: "Themenfrische (presence_penalty)",
SubTitle: SubTitle:
"Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird", "Je höher der Wert, desto wahrscheinlicher wird auf neue Themen eingegangen",
}, },
FrequencyPenalty: { FrequencyPenalty: {
Title: "Frequency Penalty", // HäufigkeitStrafe Title: "Häufigkeitsstrafe (frequency_penalty)",
SubTitle: SubTitle:
"Ein größerer Wert, der die Wahrscheinlichkeit verringert, dass dieselbe Zeile wiederholt wird", "Je höher der Wert, desto wahrscheinlicher werden wiederholte Wörter reduziert",
}, },
}, },
Store: { Store: {
DefaultTopic: "Neues Gespräch", DefaultTopic: "Neuer Chat",
BotHello: "Hallo! Wie kann ich Ihnen heute helfen?", BotHello: "Wie kann ich Ihnen helfen?",
Error: Error:
"Etwas ist schief gelaufen, bitte versuchen Sie es später noch einmal.", "Ein Fehler ist aufgetreten, bitte versuchen Sie es später noch einmal",
Prompt: { Prompt: {
History: (content: string) => History: (content: string) =>
"Dies ist eine Zusammenfassung des Chatverlaufs zwischen dem KI und dem Benutzer als Rückblick: " + "Dies ist eine Zusammenfassung des bisherigen Chats als Hintergrundinformation: " +
content, content,
Topic: Topic:
"Bitte erstellen Sie einen vier- bis fünfwörtigen Titel, der unser Gespräch zusammenfasst, ohne Einleitung, Zeichensetzung, Anführungszeichen, Punkte, Symbole oder zusätzlichen Text. Entfernen Sie Anführungszeichen.", "Geben Sie ein kurzes Thema in vier bis fünf Wörtern zurück, ohne Erklärungen, ohne Satzzeichen, ohne Füllwörter, ohne zusätzliche Texte und ohne Fettdruck. Wenn kein Thema vorhanden ist, geben Sie bitte „Allgemeines Gespräch“ zurück.",
Summarize: Summarize:
"Fassen Sie unsere Diskussion kurz in 200 Wörtern oder weniger zusammen, um sie als Pronpt für zukünftige Gespräche zu verwenden.", "Fassen Sie den Gesprächsinhalt zusammen, um als Kontextaufforderung für den nächsten Schritt zu dienen, halten Sie es unter 200 Zeichen",
}, },
}, },
Copy: { Copy: {
Success: "In die Zwischenablage kopiert", Success: "In die Zwischenablage geschrieben",
Failed: Failed:
"Kopieren fehlgeschlagen, bitte geben Sie die Berechtigung zum Zugriff auf die Zwischenablage frei", "Kopieren fehlgeschlagen, bitte erlauben Sie Zugriff auf die Zwischenablage",
},
Download: {
Success: "Inhalt wurde in Ihrem Verzeichnis heruntergeladen.",
Failed: "Download fehlgeschlagen.",
}, },
Context: { Context: {
Toast: (x: any) => `Mit ${x} Kontext-Prompts`, Toast: (x: any) => `Beinhaltet ${x} vordefinierte Eingabeaufforderungen`,
Edit: "Kontext- und Gedächtnis-Prompts", Edit: "Aktuelle Gesprächseinstellungen",
Add: "Hinzufügen", Add: "Neues Gespräch hinzufügen",
Clear: "Kontext gelöscht",
Revert: "Kontext wiederherstellen",
}, },
Plugin: { Plugin: {
Name: "Plugin", Name: "Plugins",
}, },
FineTuned: { FineTuned: {
Sysmessage: "Du bist ein Assistent, der", Sysmessage: "Du bist ein Assistent",
}, },
Mask: { SearchChat: {
Name: "Mask", Name: "Suche",
Page: { Page: {
Title: "Prompt Template", Title: "Chatverlauf durchsuchen",
SubTitle: (count: number) => `${count} prompt templates`, Search: "Suchbegriff eingeben",
Search: "Search Templates", NoResult: "Keine Ergebnisse gefunden",
Create: "Create", NoData: "Keine Daten",
Loading: "Laden",
SubTitle: (count: number) => `${count} Ergebnisse gefunden`,
}, },
Item: { Item: {
Info: (count: number) => `${count} prompts`, View: "Ansehen",
Chat: "Chat", },
View: "View", },
Edit: "Edit", Mask: {
Delete: "Delete", Name: "Masken",
DeleteConfirm: "Confirm to delete?", Page: {
Title: "Vordefinierte Rollenmasken",
SubTitle: (count: number) =>
`${count} vordefinierte Rollenbeschreibungen`,
Search: "Rollenmasken suchen",
Create: "Neu erstellen",
},
Item: {
Info: (count: number) => `Beinhaltet ${count} vordefinierte Gespräche`,
Chat: "Gespräch",
View: "Anzeigen",
Edit: "Bearbeiten",
Delete: "Löschen",
DeleteConfirm: "Bestätigen Sie das Löschen?",
}, },
EditModal: { EditModal: {
Title: (readonly: boolean) => Title: (readonly: boolean) =>
`Edit Prompt Template ${readonly ? "(readonly)" : ""}`, `Vordefinierte Maske bearbeiten ${readonly ? "Nur lesen" : ""}`,
Download: "Download", Download: "Vorgabe herunterladen",
Clone: "Clone", Clone: "Vorgabe klonen",
}, },
Config: { Config: {
Avatar: "Bot Avatar", Avatar: "Rollen-Avatar",
Name: "Bot Name", Name: "Rollenname",
Sync: {
Title: "Globale Einstellungen verwenden",
SubTitle:
"Soll das aktuelle Gespräch die globalen Modelleinstellungen verwenden?",
Confirm:
"Die benutzerdefinierten Einstellungen des aktuellen Gesprächs werden automatisch überschrieben. Bestätigen Sie, dass Sie die globalen Einstellungen aktivieren möchten?",
},
HideContext: {
Title: "Vordefinierte Gespräche ausblenden",
SubTitle:
"Nach dem Ausblenden werden vordefinierte Gespräche nicht mehr im Chat angezeigt",
},
Share: {
Title: "Diese Maske teilen",
SubTitle: "Generieren Sie einen Direktlink zu dieser Maske",
Action: "Link kopieren",
},
}, },
}, },
NewChat: { NewChat: {
Return: "Return", Return: "Zurück",
Skip: "Skip", Skip: "Direkt beginnen",
Title: "Pick a Mask", NotShow: "Nicht mehr anzeigen",
SubTitle: "Chat with the Soul behind the Mask", ConfirmNoShow:
More: "Find More", "Bestätigen Sie die Deaktivierung? Nach der Deaktivierung können Sie jederzeit in den Einstellungen wieder aktivieren.",
NotShow: "Not Show Again", Title: "Wählen Sie eine Maske aus",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.", SubTitle:
"Starten Sie jetzt und lassen Sie sich von den Gedanken hinter der Maske inspirieren",
More: "Alle anzeigen",
},
URLCommand: {
Code: "Ein Zugangscode wurde im Link gefunden. Möchten Sie diesen automatisch einfügen?",
Settings:
"Vordefinierte Einstellungen wurden im Link gefunden. Möchten Sie diese automatisch einfügen?",
}, },
UI: { UI: {
Confirm: "Confirm", Confirm: "Bestätigen",
Cancel: "Cancel", Cancel: "Abbrechen",
Close: "Close", Close: "Schließen",
Create: "Create", Create: "Neu erstellen",
Edit: "Edit", Edit: "Bearbeiten",
Export: "Exportieren",
Import: "Importieren",
Sync: "Synchronisieren",
Config: "Konfigurieren",
}, },
Exporter: { Exporter: {
Description: {
Title: "Nur Nachrichten nach dem Löschen des Kontexts werden angezeigt",
},
Model: "Modell", Model: "Modell",
Messages: "Nachrichten", Messages: "Nachrichten",
Topic: "Thema", Topic: "Thema",

View File

@ -1,7 +1,7 @@
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import { LocaleType } from "./index"; import { LocaleType } from "./index";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
// if you are adding a new translation, please use PartialLocaleType instead of LocaleType // if you are adding a new translation, please use PartialLocaleType instead of LocaleType
const isApp = !!getClientConfig()?.isApp; const isApp = !!getClientConfig()?.isApp;
@ -9,16 +9,26 @@ const en: LocaleType = {
WIP: "Coming Soon...", WIP: "Coming Soon...",
Error: { Error: {
Unauthorized: isApp Unauthorized: isApp
? "Invalid API Key, please check it in [Settings](/#/settings) page." ? `😆 Oops, there's an issue. No worries:
: "Unauthorized access, please enter access code in [auth](/#/auth) page, or enter your OpenAI API Key.", \\ 1 New here? [Click to start chatting now 🚀](${SAAS_CHAT_UTM_URL})
\\ 2 Want to use your own OpenAI resources? [Click here](/#/settings) to change settings `
: `😆 Oops, there's an issue. Let's fix it:
\ 1 New here? [Click to start chatting now 🚀](${SAAS_CHAT_UTM_URL})
\ 2 Using a private setup? [Click here](/#/auth) to enter your key 🔑
\ 3 Want to use your own OpenAI resources? [Click here](/#/settings) to change settings
`,
}, },
Auth: { Auth: {
Return: "Return",
Title: "Need Access Code", Title: "Need Access Code",
Tips: "Please enter access code below", Tips: "Please enter access code below",
SubTips: "Or enter your OpenAI or Google API Key", SubTips: "Or enter your OpenAI or Google API Key",
Input: "access code", Input: "access code",
Confirm: "Confirm", Confirm: "Confirm",
Later: "Later", Later: "Later",
SaasTips: "Too Complex, Use Immediately Now",
TopTips:
"🥳 NextChat AI launch promotion: Instantly unlock the latest models like OpenAI o1, GPT-4o, Claude-3.5!",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} messages`, ChatItemCount: (count: number) => `${count} messages`,
@ -44,6 +54,11 @@ const en: LocaleType = {
PinToastAction: "View", PinToastAction: "View",
Delete: "Delete", Delete: "Delete",
Edit: "Edit", Edit: "Edit",
FullScreen: "FullScreen",
RefreshTitle: "Refresh Title",
RefreshToast: "Title refresh request sent",
Speech: "Play",
StopSpeech: "Stop",
}, },
Commands: { Commands: {
new: "Start a new chat", new: "Start a new chat",
@ -51,6 +66,7 @@ const en: LocaleType = {
next: "Next Chat", next: "Next Chat",
prev: "Previous Chat", prev: "Previous Chat",
clear: "Clear Context", clear: "Clear Context",
fork: "Copy Chat",
del: "Delete Chat", del: "Delete Chat",
}, },
InputActions: { InputActions: {
@ -77,11 +93,21 @@ const en: LocaleType = {
return inputHints + ", / to search prompts, : to use commands"; return inputHints + ", / to search prompts, : to use commands";
}, },
Send: "Send", Send: "Send",
StartSpeak: "Start Speak",
StopSpeak: "Stop Speak",
Config: { Config: {
Reset: "Reset to Default", Reset: "Reset to Default",
SaveAs: "Save as Mask", SaveAs: "Save as Mask",
}, },
IsContext: "Contextual Prompt", IsContext: "Contextual Prompt",
ShortcutKey: {
Title: "Keyboard Shortcuts",
newChat: "Open New Chat",
focusInput: "Focus Input Field",
copyLastMessage: "Copy Last Reply",
copyLastCode: "Copy Last Code Block",
showShortcutKey: "Show Shortcuts",
},
}, },
Export: { Export: {
Title: "Export Messages", Title: "Export Messages",
@ -106,6 +132,10 @@ const en: LocaleType = {
Toast: "Capturing Image...", Toast: "Capturing Image...",
Modal: "Long press or right click to save image", Modal: "Long press or right click to save image",
}, },
Artifacts: {
Title: "Share Artifacts",
Error: "Share Error",
},
}, },
Select: { Select: {
Search: "Search", Search: "Search",
@ -131,6 +161,7 @@ const en: LocaleType = {
Settings: { Settings: {
Title: "Settings", Title: "Settings",
SubTitle: "All Settings", SubTitle: "All Settings",
ShowPassword: "ShowPassword",
Danger: { Danger: {
Reset: { Reset: {
Title: "Reset All Settings", Title: "Reset All Settings",
@ -154,6 +185,12 @@ const en: LocaleType = {
Title: "Font Size", Title: "Font Size",
SubTitle: "Adjust font size of chat content", SubTitle: "Adjust font size of chat content",
}, },
FontFamily: {
Title: "Chat Font Family",
SubTitle:
"Font Family of the chat content, leave empty to apply global default font",
Placeholder: "Font Family Name",
},
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "Inject System Prompts", Title: "Inject System Prompts",
SubTitle: "Inject a global system prompt for every request", SubTitle: "Inject a global system prompt for every request",
@ -170,6 +207,8 @@ const en: LocaleType = {
IsChecking: "Checking update...", IsChecking: "Checking update...",
FoundUpdate: (x: string) => `Found new version: ${x}`, FoundUpdate: (x: string) => `Found new version: ${x}`,
GoToUpdate: "Update", GoToUpdate: "Update",
Success: "Update Successful.",
Failed: "Update Failed.",
}, },
SendKey: "Send Key", SendKey: "Send Key",
Theme: "Theme", Theme: "Theme",
@ -274,6 +313,14 @@ const en: LocaleType = {
NoAccess: "Enter API Key to check balance", NoAccess: "Enter API Key to check balance",
}, },
Access: { Access: {
SaasStart: {
Title: "Use NextChat AI",
Label: " (Most Cost-Effective Option)",
SubTitle:
"Maintained by NextChat, zero setup needed, unlock OpenAI o1, GPT-4o," +
" Claude-3.5 and more",
ChatNow: "Start Now",
},
AccessCode: { AccessCode: {
Title: "Access Code", Title: "Access Code",
SubTitle: "Access control Enabled", SubTitle: "Access control Enabled",
@ -350,6 +397,22 @@ const en: LocaleType = {
SubTitle: "not supported, configure in .env", SubTitle: "not supported, configure in .env",
}, },
}, },
Tencent: {
ApiKey: {
Title: "Tencent API Key",
SubTitle: "Use a custom Tencent API Key",
Placeholder: "Tencent API Key",
},
SecretKey: {
Title: "Tencent Secret Key",
SubTitle: "Use a custom Tencent Secret Key",
Placeholder: "Tencent Secret Key",
},
Endpoint: {
Title: "Endpoint Address",
SubTitle: "not supported, configure in .env",
},
},
ByteDance: { ByteDance: {
ApiKey: { ApiKey: {
Title: "ByteDance API Key", Title: "ByteDance API Key",
@ -372,6 +435,55 @@ const en: LocaleType = {
SubTitle: "Example: ", SubTitle: "Example: ",
}, },
}, },
Moonshot: {
ApiKey: {
Title: "Moonshot API Key",
SubTitle: "Use a custom Moonshot API Key",
Placeholder: "Moonshot API Key",
},
Endpoint: {
Title: "Endpoint Address",
SubTitle: "Example: ",
},
},
XAI: {
ApiKey: {
Title: "XAI API Key",
SubTitle: "Use a custom XAI API Key",
Placeholder: "XAI API Key",
},
Endpoint: {
Title: "Endpoint Address",
SubTitle: "Example: ",
},
},
Stability: {
ApiKey: {
Title: "Stability API Key",
SubTitle: "Use a custom Stability API Key",
Placeholder: "Stability API Key",
},
Endpoint: {
Title: "Endpoint Address",
SubTitle: "Example: ",
},
},
Iflytek: {
ApiKey: {
Title: "Iflytek API Key",
SubTitle: "Use a Iflytek API Key",
Placeholder: "Iflytek API Key",
},
ApiSecret: {
Title: "Iflytek API Secret",
SubTitle: "Use a Iflytek API Secret",
Placeholder: "Iflytek API Secret",
},
Endpoint: {
Title: "Endpoint Address",
SubTitle: "Example: ",
},
},
CustomModel: { CustomModel: {
Title: "Custom Models", Title: "Custom Models",
SubTitle: "Custom model options, seperated by comma", SubTitle: "Custom model options, seperated by comma",
@ -380,7 +492,7 @@ const en: LocaleType = {
ApiKey: { ApiKey: {
Title: "API Key", Title: "API Key",
SubTitle: "Obtain your API Key from Google AI", SubTitle: "Obtain your API Key from Google AI",
Placeholder: "Enter your Google AI Studio API Key", Placeholder: "Google AI API Key",
}, },
Endpoint: { Endpoint: {
@ -392,10 +504,18 @@ const en: LocaleType = {
Title: "API Version (specific to gemini-pro)", Title: "API Version (specific to gemini-pro)",
SubTitle: "Select a specific API version", SubTitle: "Select a specific API version",
}, },
GoogleSafetySettings: {
Title: "Google Safety Settings",
SubTitle: "Select a safety filtering level",
},
}, },
}, },
Model: "Model", Model: "Model",
CompressModel: {
Title: "Summary Model",
SubTitle: "Model used to compress history and generate title",
},
Temperature: { Temperature: {
Title: "Temperature", Title: "Temperature",
SubTitle: "A larger value makes the more random output", SubTitle: "A larger value makes the more random output",
@ -418,6 +538,27 @@ const en: LocaleType = {
SubTitle: SubTitle:
"A larger value decreasing the likelihood to repeat the same line", "A larger value decreasing the likelihood to repeat the same line",
}, },
TTS: {
Enable: {
Title: "Enable TTS",
SubTitle: "Enable text-to-speech service",
},
Autoplay: {
Title: "Enable Autoplay",
SubTitle:
"Automatically generate speech and play, you need to enable the text-to-speech switch first",
},
Model: "Model",
Voice: {
Title: "Voice",
SubTitle: "The voice to use when generating the audio",
},
Speed: {
Title: "Speed",
SubTitle: "The speed of the generated audio",
},
Engine: "TTS Engine",
},
}, },
Store: { Store: {
DefaultTopic: "New Conversation", DefaultTopic: "New Conversation",
@ -447,12 +588,68 @@ const en: LocaleType = {
Clear: "Context Cleared", Clear: "Context Cleared",
Revert: "Revert", Revert: "Revert",
}, },
Plugin: { Discovery: {
Name: "Plugin", Name: "Discovery",
}, },
FineTuned: { FineTuned: {
Sysmessage: "You are an assistant that", Sysmessage: "You are an assistant that",
}, },
SearchChat: {
Name: "Search",
Page: {
Title: "Search Chat History",
Search: "Enter search query to search chat history",
NoResult: "No results found",
NoData: "No data",
Loading: "Loading...",
SubTitle: (count: number) => `Found ${count} results`,
},
Item: {
View: "View",
},
},
Plugin: {
Name: "Plugin",
Page: {
Title: "Plugins",
SubTitle: (count: number) => `${count} plugins`,
Search: "Search Plugin",
Create: "Create",
Find: "You can find awesome plugins on github: ",
},
Item: {
Info: (count: number) => `${count} method`,
View: "View",
Edit: "Edit",
Delete: "Delete",
DeleteConfirm: "Confirm to delete?",
},
Auth: {
None: "None",
Basic: "Basic",
Bearer: "Bearer",
Custom: "Custom",
CustomHeader: "Parameter Name",
Token: "Token",
Proxy: "Using Proxy",
ProxyDescription: "Using proxies to solve CORS error",
Location: "Location",
LocationHeader: "Header",
LocationQuery: "Query",
LocationBody: "Body",
},
EditModal: {
Title: (readonly: boolean) =>
`Edit Plugin ${readonly ? "(readonly)" : ""}`,
Download: "Download",
Auth: "Authentication Type",
Content: "OpenAPI Schema",
Load: "Load From URL",
Method: "Method",
Error: "OpenAPI Schema Error",
},
},
Mask: { Mask: {
Name: "Mask", Name: "Mask",
Page: { Page: {
@ -487,6 +684,15 @@ const en: LocaleType = {
Title: "Hide Context Prompts", Title: "Hide Context Prompts",
SubTitle: "Do not show in-context prompts in chat", SubTitle: "Do not show in-context prompts in chat",
}, },
Artifacts: {
Title: "Enable Artifacts",
SubTitle: "Can render HTML page when enable artifacts.",
},
CodeFold: {
Title: "Enable CodeFold",
SubTitle:
"Automatically collapse/expand overly long code blocks when CodeFold is enabled",
},
Share: { Share: {
Title: "Share This Mask", Title: "Share This Mask",
SubTitle: "Generate a link to this mask", SubTitle: "Generate a link to this mask",
@ -524,11 +730,65 @@ const en: LocaleType = {
Topic: "Topic", Topic: "Topic",
Time: "Time", Time: "Time",
}, },
URLCommand: { URLCommand: {
Code: "Detected access code from url, confirm to apply? ", Code: "Detected access code from url, confirm to apply? ",
Settings: "Detected settings from url, confirm to apply?", Settings: "Detected settings from url, confirm to apply?",
}, },
SdPanel: {
Prompt: "Prompt",
NegativePrompt: "Negative Prompt",
PleaseInput: (name: string) => `Please input ${name}`,
AspectRatio: "Aspect Ratio",
ImageStyle: "Image Style",
OutFormat: "Output Format",
AIModel: "AI Model",
ModelVersion: "Model Version",
Submit: "Submit",
ParamIsRequired: (name: string) => `${name} is required`,
Styles: {
D3Model: "3d-model",
AnalogFilm: "analog-film",
Anime: "anime",
Cinematic: "cinematic",
ComicBook: "comic-book",
DigitalArt: "digital-art",
Enhance: "enhance",
FantasyArt: "fantasy-art",
Isometric: "isometric",
LineArt: "line-art",
LowPoly: "low-poly",
ModelingCompound: "modeling-compound",
NeonPunk: "neon-punk",
Origami: "origami",
Photographic: "photographic",
PixelArt: "pixel-art",
TileTexture: "tile-texture",
},
},
Sd: {
SubTitle: (count: number) => `${count} images`,
Actions: {
Params: "See Params",
Copy: "Copy Prompt",
Delete: "Delete",
Retry: "Retry",
ReturnHome: "Return Home",
History: "History",
},
EmptyRecord: "No images yet",
Status: {
Name: "Status",
Success: "Success",
Error: "Error",
Wait: "Waiting",
Running: "Running",
},
Danger: {
Delete: "Confirm to delete?",
},
GenerateParams: "Generate Params",
Detail: "Detail",
},
}; };
export default en; export default en;

View File

@ -1,239 +1,605 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index"; import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
const es: PartialLocaleType = { const es: PartialLocaleType = {
WIP: "En construcción...", WIP: "En construcción...",
Error: { Error: {
Unauthorized: Unauthorized: isApp
"Acceso no autorizado, por favor ingrese el código de acceso en la [página](/#/auth) de configuración.", ? `😆 La conversación encontró algunos problemas, no te preocupes:
\\ 1 Si deseas comenzar sin configuración, [haz clic aquí para empezar a chatear inmediatamente 🚀](${SAAS_CHAT_UTM_URL})
\\ 2 Si deseas usar tus propios recursos de OpenAI, haz clic [aquí](/#/settings) para modificar la configuración `
: `😆 La conversación encontró algunos problemas, no te preocupes:
\ 1 Si deseas comenzar sin configuración, [haz clic aquí para empezar a chatear inmediatamente 🚀](${SAAS_CHAT_UTM_URL})
\ 2 Si estás utilizando una versión de implementación privada, haz clic [aquí](/#/auth) para ingresar la clave de acceso 🔑
\ 3 Si deseas usar tus propios recursos de OpenAI, haz clic [aquí](/#/settings) para modificar la configuración
`,
},
Auth: {
Title: "Se requiere contraseña",
Tips: "El administrador ha habilitado la verificación de contraseña. Introduce el código de acceso a continuación",
SubTips: "O ingresa tu clave API de OpenAI o Google",
Input: "Introduce el código de acceso aquí",
Confirm: "Confirmar",
Later: "Más tarde",
Return: "Regresar",
SaasTips:
"La configuración es demasiado complicada, quiero usarlo de inmediato",
TopTips:
"🥳 Oferta de lanzamiento de NextChat AI, desbloquea OpenAI o1, GPT-4o, Claude-3.5 y los últimos grandes modelos",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} mensajes`, ChatItemCount: (count: number) => `${count} conversaciones`,
}, },
Chat: { Chat: {
SubTitle: (count: number) => `${count} mensajes con ChatGPT`, SubTitle: (count: number) => `Total de ${count} conversaciones`,
EditMessage: {
Title: "Editar registro de mensajes",
Topic: {
Title: "Tema de la conversación",
SubTitle: "Cambiar el tema de la conversación actual",
},
},
Actions: { Actions: {
ChatList: "Ir a la lista de chats", ChatList: "Ver lista de mensajes",
CompressedHistory: "Historial de memoria comprimido", CompressedHistory: "Ver historial de Prompts comprimidos",
Export: "Exportar todos los mensajes como Markdown", Export: "Exportar historial de chat",
Copy: "Copiar", Copy: "Copiar",
Stop: "Detener", Stop: "Detener",
Retry: "Reintentar", Retry: "Reintentar",
Delete: "Delete", Pin: "Fijar",
PinToastContent:
"Se ha fijado 1 conversación a los prompts predeterminados",
PinToastAction: "Ver",
Delete: "Eliminar",
Edit: "Editar",
RefreshTitle: "Actualizar título",
RefreshToast: "Se ha enviado la solicitud de actualización del título",
}, },
Rename: "Renombrar chat", Commands: {
Typing: "Escribiendo...", new: "Nueva conversación",
newm: "Nueva conversación desde la máscara",
next: "Siguiente conversación",
prev: "Conversación anterior",
clear: "Limpiar contexto",
del: "Eliminar conversación",
},
InputActions: {
Stop: "Detener respuesta",
ToBottom: "Ir al más reciente",
Theme: {
auto: "Tema automático",
light: "Modo claro",
dark: "Modo oscuro",
},
Prompt: "Comandos rápidos",
Masks: "Todas las máscaras",
Clear: "Limpiar chat",
Settings: "Configuración de conversación",
UploadImage: "Subir imagen",
},
Rename: "Renombrar conversación",
Typing: "Escribiendo…",
Input: (submitKey: string) => { Input: (submitKey: string) => {
var inputHints = `Escribe algo y presiona ${submitKey} para enviar`; var inputHints = `${submitKey} para enviar`;
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", presiona Shift + Enter para nueva línea"; inputHints += "Shift + Enter para nueva línea";
} }
return inputHints; return (
inputHints + "/ para activar autocompletado: para activar comandos"
);
}, },
Send: "Enviar", Send: "Enviar",
Config: { Config: {
Reset: "Reset to Default", Reset: "Borrar memoria",
SaveAs: "Save as Mask", SaveAs: "Guardar como máscara",
}, },
IsContext: "Prompt predeterminado",
}, },
Export: { Export: {
Title: "Todos los mensajes", Title: "Compartir historial de chat",
Copy: "Copiar todo", Copy: "Copiar todo",
Download: "Descargar", Download: "Descargar archivo",
MessageFromYou: "Mensaje de ti", Share: "Compartir en ShareGPT",
MessageFromChatGPT: "Mensaje de ChatGPT", MessageFromYou: "Usuario",
MessageFromChatGPT: "ChatGPT",
Format: {
Title: "Formato de exportación",
SubTitle: "Puedes exportar como texto Markdown o imagen PNG",
},
IncludeContext: {
Title: "Incluir contexto de máscara",
SubTitle: "Mostrar contexto de máscara en los mensajes",
},
Steps: {
Select: "Seleccionar",
Preview: "Vista previa",
},
Image: {
Toast: "Generando captura de pantalla",
Modal: "Mantén presionado o haz clic derecho para guardar la imagen",
},
},
Select: {
Search: "Buscar mensajes",
All: "Seleccionar todo",
Latest: "Últimos mensajes",
Clear: "Limpiar selección",
}, },
Memory: { Memory: {
Title: "Historial de memoria", Title: "Resumen histórico",
EmptyContent: "Aún no hay nada.", EmptyContent:
Copy: "Copiar todo", "El contenido de la conversación es demasiado corto para resumir",
Send: "Send Memory", Send: "Comprimir automáticamente el historial de chat y enviarlo como contexto",
Reset: "Reset Session", Copy: "Copiar resumen",
ResetConfirm: Reset: "[no usado]",
"Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?", ResetConfirm: "¿Confirmar para borrar el resumen histórico?",
}, },
Home: { Home: {
NewChat: "Nuevo chat", NewChat: "Nueva conversación",
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?", DeleteChat: "¿Confirmar la eliminación de la conversación seleccionada?",
DeleteToast: "Chat Deleted", DeleteToast: "Conversación eliminada",
Revert: "Revert", Revert: "Deshacer",
}, },
Settings: { Settings: {
Title: "Configuración", Title: "Configuración",
SubTitle: "Todas las configuraciones", SubTitle: "Todas las opciones de configuración",
Danger: {
Reset: {
Title: "Restablecer todas las configuraciones",
SubTitle:
"Restablecer todas las configuraciones a los valores predeterminados",
Action: "Restablecer ahora",
Confirm: "¿Confirmar el restablecimiento de todas las configuraciones?",
},
Clear: {
Title: "Eliminar todos los datos",
SubTitle: "Eliminar todos los chats y datos de configuración",
Action: "Eliminar ahora",
Confirm:
"¿Confirmar la eliminación de todos los chats y datos de configuración?",
},
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATENCIÓN: si deseas agregar una nueva traducción, por favor no traduzcas este valor, déjalo como `Language`
All: "Todos los idiomas", All: "Todos los idiomas",
}, },
Avatar: "Avatar", Avatar: "Avatar",
FontSize: { FontSize: {
Title: "Tamaño de fuente", Title: "Tamaño de fuente",
SubTitle: "Ajustar el tamaño de fuente del contenido del chat", SubTitle: "Tamaño de la fuente del contenido del chat",
},
FontFamily: {
Title: "Fuente del Chat",
SubTitle:
"Fuente del contenido del chat, dejar vacío para aplicar la fuente predeterminada global",
Placeholder: "Nombre de la Fuente",
}, },
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "Inyectar Prompts del Sistema", Title: "Inyectar mensajes del sistema",
SubTitle: SubTitle:
"Agregar forzosamente un prompt de sistema simulado de ChatGPT al comienzo de la lista de mensajes en cada solicitud", "Forzar la adición de un mensaje del sistema simulado de ChatGPT al principio de cada lista de mensajes",
}, },
InputTemplate: {
Title: "Preprocesamiento de entrada del usuario",
SubTitle: "El último mensaje del usuario se rellenará en esta plantilla",
},
Update: { Update: {
Version: (x: string) => `Versión: ${x}`, Version: (x: string) => `Versión actual: ${x}`,
IsLatest: "Última versión", IsLatest: "Ya estás en la última versión",
CheckUpdate: "Buscar actualizaciones", CheckUpdate: "Buscar actualizaciones",
IsChecking: "Buscando actualizaciones...", IsChecking: "Buscando actualizaciones...",
FoundUpdate: (x: string) => `Se encontró una nueva versión: ${x}`, FoundUpdate: (x: string) => `Nueva versión encontrada: ${x}`,
GoToUpdate: "Actualizar", GoToUpdate: "Ir a actualizar",
}, },
SendKey: "Tecla de envío", SendKey: "Tecla de enviar",
Theme: "Tema", Theme: "Tema",
TightBorder: "Borde ajustado", TightBorder: "Modo sin borde",
SendPreviewBubble: { SendPreviewBubble: {
Title: "Enviar burbuja de vista previa", Title: "Vista previa del globo",
SubTitle: "Preview markdown in bubble", SubTitle:
"Previsualiza el contenido Markdown en un globo de vista previa",
},
AutoGenerateTitle: {
Title: "Generar título automáticamente",
SubTitle: "Generar un título adecuado basado en el contenido del chat",
},
Sync: {
CloudState: "Datos en la nube",
NotSyncYet: "Aún no se ha sincronizado",
Success: "Sincronización exitosa",
Fail: "Sincronización fallida",
Config: {
Modal: {
Title: "Configurar sincronización en la nube",
Check: "Verificar disponibilidad",
},
SyncType: {
Title: "Tipo de sincronización",
SubTitle: "Selecciona el servidor de sincronización preferido",
},
Proxy: {
Title: "Habilitar proxy",
SubTitle:
"Debes habilitar el proxy para sincronizar en el navegador y evitar restricciones de CORS",
},
ProxyUrl: {
Title: "Dirección del proxy",
SubTitle: "Solo para el proxy CORS incluido en este proyecto",
},
WebDav: {
Endpoint: "Dirección WebDAV",
UserName: "Nombre de usuario",
Password: "Contraseña",
},
UpStash: {
Endpoint: "URL de REST de UpStash Redis",
UserName: "Nombre de respaldo",
Password: "Token de REST de UpStash Redis",
},
},
LocalState: "Datos locales",
Overview: (overview: any) => {
return `${overview.chat} conversaciones, ${overview.message} mensajes, ${overview.prompt} prompts, ${overview.mask} máscaras`;
},
ImportFailed: "Importación fallida",
}, },
Mask: { Mask: {
Splash: { Splash: {
Title: "Mask Splash Screen", Title: "Pantalla de inicio de máscara",
SubTitle: "Show a mask splash screen before starting new chat", SubTitle:
"Mostrar la pantalla de inicio de la máscara al iniciar un nuevo chat",
},
Builtin: {
Title: "Ocultar máscaras integradas",
SubTitle:
"Ocultar las máscaras integradas en todas las listas de máscaras",
}, },
}, },
Prompt: { Prompt: {
Disable: { Disable: {
Title: "Desactivar autocompletado", Title: "Deshabilitar autocompletado de prompts",
SubTitle: "Escribe / para activar el autocompletado", SubTitle:
"Escribe / al principio del campo de entrada para activar el autocompletado",
}, },
List: "Lista de autocompletado", List: "Lista de prompts personalizados",
ListCount: (builtin: number, custom: number) => ListCount: (builtin: number, custom: number) =>
`${builtin} incorporado, ${custom} definido por el usuario`, `Integrados ${builtin}, definidos por el usuario ${custom}`,
Edit: "Editar", Edit: "Editar",
Modal: { Modal: {
Title: "Prompt List", Title: "Lista de prompts",
Add: "Add One", Add: "Nuevo",
Search: "Search Prompts", Search: "Buscar prompts",
}, },
EditModal: { EditModal: {
Title: "Edit Prompt", Title: "Editar prompt",
}, },
}, },
HistoryCount: { HistoryCount: {
Title: "Cantidad de mensajes adjuntos", Title: "Número de mensajes históricos adjuntos",
SubTitle: "Número de mensajes enviados adjuntos por solicitud", SubTitle: "Número de mensajes históricos enviados con cada solicitud",
}, },
CompressThreshold: { CompressThreshold: {
Title: "Umbral de compresión de historial", Title: "Umbral de compresión de mensajes históricos",
SubTitle: SubTitle:
"Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor", "Cuando los mensajes históricos no comprimidos superan este valor, se realizará la compresión",
}, },
Usage: { Usage: {
Title: "Saldo de la cuenta", Title: "Consulta de saldo",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
return `Usado $${used}, subscription $${total}`; return `Saldo usado este mes: $${used}, total suscrito: $${total}`;
}, },
IsChecking: "Comprobando...", IsChecking: "Verificando…",
Check: "Comprobar de nuevo", Check: "Revisar de nuevo",
NoAccess: "Introduzca la clave API para comprobar el saldo", NoAccess:
"Introduce la clave API o la contraseña de acceso para ver el saldo",
}, },
Model: "Modelo", Access: {
SaasStart: {
Title: "Use NextChat AI",
Label: "(The most cost-effective solution)",
SubTitle:
"Officially maintained by NextChat, zero configuration ready to use, supports the latest large models like OpenAI o1, GPT-4o, and Claude-3.5",
ChatNow: "Chat Now",
},
AccessCode: {
Title: "Contraseña de acceso",
SubTitle: "El administrador ha habilitado el acceso encriptado",
Placeholder: "Introduce la contraseña de acceso",
},
CustomEndpoint: {
Title: "Interfaz personalizada",
SubTitle: "¿Usar servicios personalizados de Azure u OpenAI?",
},
Provider: {
Title: "Proveedor de modelos",
SubTitle: "Cambiar entre diferentes proveedores",
},
OpenAI: {
ApiKey: {
Title: "Clave API",
SubTitle:
"Usa una clave API de OpenAI personalizada para omitir la restricción de acceso por contraseña",
Placeholder: "Clave API de OpenAI",
},
Endpoint: {
Title: "Dirección del endpoint",
SubTitle:
"Debe incluir http(s):// además de la dirección predeterminada",
},
},
Azure: {
ApiKey: {
Title: "Clave de interfaz",
SubTitle:
"Usa una clave de Azure personalizada para omitir la restricción de acceso por contraseña",
Placeholder: "Clave API de Azure",
},
Endpoint: {
Title: "Dirección del endpoint",
SubTitle: "Ejemplo:",
},
ApiVerion: {
Title: "Versión de la interfaz (versión de api de azure)",
SubTitle: "Selecciona una versión específica",
},
},
Anthropic: {
ApiKey: {
Title: "Clave de interfaz",
SubTitle:
"Usa una clave de Anthropic personalizada para omitir la restricción de acceso por contraseña",
Placeholder: "Clave API de Anthropic",
},
Endpoint: {
Title: "Dirección del endpoint",
SubTitle: "Ejemplo:",
},
ApiVerion: {
Title: "Versión de la interfaz (versión de claude api)",
SubTitle: "Selecciona una versión específica de la API",
},
},
Google: {
ApiKey: {
Title: "Clave API",
SubTitle: "Obtén tu clave API de Google AI",
Placeholder: "Introduce tu clave API de Google AI Studio",
},
Endpoint: {
Title: "Dirección del endpoint",
SubTitle: "Ejemplo:",
},
ApiVersion: {
Title: "Versión de la API (solo para gemini-pro)",
SubTitle: "Selecciona una versión específica de la API",
},
GoogleSafetySettings: {
Title: "Nivel de filtrado de seguridad de Google",
SubTitle: "Configura el nivel de filtrado de contenido",
},
},
Baidu: {
ApiKey: {
Title: "Clave API",
SubTitle: "Usa una clave API de Baidu personalizada",
Placeholder: "Clave API de Baidu",
},
SecretKey: {
Title: "Clave secreta",
SubTitle: "Usa una clave secreta de Baidu personalizada",
Placeholder: "Clave secreta de Baidu",
},
Endpoint: {
Title: "Dirección del endpoint",
SubTitle:
"No admite personalización, dirígete a .env para configurarlo",
},
},
ByteDance: {
ApiKey: {
Title: "Clave de interfaz",
SubTitle: "Usa una clave API de ByteDance personalizada",
Placeholder: "Clave API de ByteDance",
},
Endpoint: {
Title: "Dirección del endpoint",
SubTitle: "Ejemplo:",
},
},
Alibaba: {
ApiKey: {
Title: "Clave de interfaz",
SubTitle: "Usa una clave API de Alibaba Cloud personalizada",
Placeholder: "Clave API de Alibaba Cloud",
},
Endpoint: {
Title: "Dirección del endpoint",
SubTitle: "Ejemplo:",
},
},
CustomModel: {
Title: "Nombre del modelo personalizado",
SubTitle:
"Agrega opciones de modelos personalizados, separados por comas",
},
},
Model: "Modelo (model)",
CompressModel: {
Title: "Modelo de compresión",
SubTitle: "Modelo utilizado para comprimir el historial",
},
Temperature: { Temperature: {
Title: "Temperatura", Title: "Aleatoriedad (temperature)",
SubTitle: "Un valor mayor genera una salida más aleatoria", SubTitle: "Cuanto mayor sea el valor, más aleatorio será el resultado",
},
TopP: {
Title: "Muestreo por núcleo (top_p)",
SubTitle: "Similar a la aleatoriedad, pero no cambies ambos a la vez",
}, },
MaxTokens: { MaxTokens: {
Title: "Máximo de tokens", Title: "Límite de tokens por respuesta (max_tokens)",
SubTitle: "Longitud máxima de tokens de entrada y tokens generados", SubTitle: "Número máximo de tokens utilizados en una sola interacción",
}, },
PresencePenalty: { PresencePenalty: {
Title: "Penalización de presencia", Title: "Novedad de temas (presence_penalty)",
SubTitle: SubTitle:
"Un valor mayor aumenta la probabilidad de hablar sobre nuevos temas", "Cuanto mayor sea el valor, más probable es que se amplíen a nuevos temas",
}, },
FrequencyPenalty: { FrequencyPenalty: {
Title: "Penalización de frecuencia", Title: "Penalización de frecuencia (frequency_penalty)",
SubTitle: SubTitle:
"Un valor mayor que disminuye la probabilidad de repetir la misma línea", "Cuanto mayor sea el valor, más probable es que se reduzcan las palabras repetidas",
}, },
}, },
Store: { Store: {
DefaultTopic: "Nueva conversación", DefaultTopic: "Nuevo chat",
BotHello: "¡Hola! ¿Cómo puedo ayudarte hoy?", BotHello: "¿En qué puedo ayudarte?",
Error: "Algo salió mal, por favor intenta nuevamente más tarde.", Error: "Hubo un error, inténtalo de nuevo más tarde",
Prompt: { Prompt: {
History: (content: string) => History: (content: string) =>
"Este es un resumen del historial del chat entre la IA y el usuario como recapitulación: " + "Este es un resumen del chat histórico como referencia: " + content,
content,
Topic: Topic:
"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.", "Devuelve un tema breve de esta frase en cuatro a cinco palabras, sin explicación, sin puntuación, sin muletillas, sin texto adicional, sin negritas. Si no hay tema, devuelve 'charlas casuales'",
Summarize: Summarize:
"Resuma nuestra discusión brevemente en 200 caracteres o menos para usarlo como un recordatorio para futuros contextos.", "Resume brevemente el contenido de la conversación para usar como un prompt de contexto, manteniéndolo dentro de 200 palabras",
}, },
}, },
Copy: { Copy: {
Success: "Copiado al portapapeles", Success: "Copiado al portapapeles",
Failed: Failed: "Error al copiar, por favor otorga permisos al portapapeles",
"La copia falló, por favor concede permiso para acceder al portapapeles", },
Download: {
Success: "Contenido descargado en tu directorio.",
Failed: "Error al descargar.",
}, },
Context: { Context: {
Toast: (x: any) => `With ${x} contextual prompts`, Toast: (x: any) => `Contiene ${x} prompts predefinidos`,
Edit: "Contextual and Memory Prompts", Edit: "Configuración del chat actual",
Add: "Add One", Add: "Agregar una conversación",
Clear: "Contexto borrado",
Revert: "Restaurar contexto",
}, },
Plugin: { Plugin: {
Name: "Plugin", Name: "Complemento",
}, },
FineTuned: { FineTuned: {
Sysmessage: "Eres un asistente que", Sysmessage: "Eres un asistente",
}, },
Mask: { SearchChat: {
Name: "Mask", Name: "Buscar",
Page: { Page: {
Title: "Prompt Template", Title: "Buscar en el historial de chat",
SubTitle: (count: number) => `${count} prompt templates`, Search: "Ingrese la palabra clave de búsqueda",
Search: "Search Templates", NoResult: "No se encontraron resultados",
Create: "Create", NoData: "Sin datos",
Loading: "Cargando",
SubTitle: (count: number) => `Se encontraron ${count} resultados`,
}, },
Item: { Item: {
Info: (count: number) => `${count} prompts`, View: "Ver",
},
},
Mask: {
Name: "Máscara",
Page: {
Title: "Máscaras de rol predefinidas",
SubTitle: (count: number) => `${count} definiciones de rol predefinidas`,
Search: "Buscar máscara de rol",
Create: "Crear nuevo",
},
Item: {
Info: (count: number) => `Contiene ${count} conversaciones predefinidas`,
Chat: "Chat", Chat: "Chat",
View: "View", View: "Ver",
Edit: "Edit", Edit: "Editar",
Delete: "Delete", Delete: "Eliminar",
DeleteConfirm: "Confirm to delete?", DeleteConfirm: "¿Confirmar eliminación?",
}, },
EditModal: { EditModal: {
Title: (readonly: boolean) => Title: (readonly: boolean) =>
`Edit Prompt Template ${readonly ? "(readonly)" : ""}`, `Editar máscara predefinida ${readonly ? "solo lectura" : ""}`,
Download: "Download", Download: "Descargar predefinido",
Clone: "Clone", Clone: "Clonar predefinido",
}, },
Config: { Config: {
Avatar: "Bot Avatar", Avatar: "Avatar del rol",
Name: "Bot Name", Name: "Nombre del rol",
Sync: {
Title: "Usar configuración global",
SubTitle:
"¿Usar la configuración global del modelo para la conversación actual?",
Confirm:
"La configuración personalizada de la conversación actual se sobrescribirá automáticamente, ¿confirmar habilitar la configuración global?",
},
HideContext: {
Title: "Ocultar conversaciones predefinidas",
SubTitle:
"Las conversaciones predefinidas ocultas no aparecerán en la interfaz de chat",
},
Share: {
Title: "Compartir esta máscara",
SubTitle: "Generar un enlace directo a esta máscara",
Action: "Copiar enlace",
},
}, },
}, },
NewChat: { NewChat: {
Return: "Return", Return: "Regresar",
Skip: "Skip", Skip: "Comenzar ahora",
Title: "Pick a Mask", NotShow: "No mostrar más",
SubTitle: "Chat with the Soul behind the Mask", ConfirmNoShow:
More: "Find More", "¿Confirmar desactivación? Puedes reactivar en la configuración en cualquier momento.",
NotShow: "Not Show Again", Title: "Selecciona una máscara",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.", SubTitle: "Comienza ahora y colisiona con la mente detrás de la máscara",
More: "Ver todo",
},
URLCommand: {
Code: "Detectado un código de acceso en el enlace, ¿deseas autocompletarlo?",
Settings:
"Detectada configuración predefinida en el enlace, ¿deseas autocompletarla?",
}, },
UI: { UI: {
Confirm: "Confirm", Confirm: "Confirmar",
Cancel: "Cancel", Cancel: "Cancelar",
Close: "Close", Close: "Cerrar",
Create: "Create", Create: "Crear",
Edit: "Edit", Edit: "Editar",
Export: "Exportar",
Import: "Importar",
Sync: "Sincronizar",
Config: "Configurar",
}, },
Exporter: { Exporter: {
Description: {
Title: "Solo se mostrarán los mensajes después de borrar el contexto",
},
Model: "Modelo", Model: "Modelo",
Messages: "Mensajes", Messages: "Mensajes",
Topic: "Tema", Topic: "Tema",
Time: "Time", Time: "Hora",
}, },
}; };

View File

@ -1,294 +1,584 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index"; import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
const fr: PartialLocaleType = { const fr: PartialLocaleType = {
WIP: "Prochainement...", WIP: "Prochainement...",
Error: { Error: {
Unauthorized: Unauthorized: isApp
"Accès non autorisé, veuillez saisir le code d'accès dans la [page](/#/auth) des paramètres.", ? `😆 La conversation a rencontré quelques problèmes, pas de panique :
\\ 1 Si vous souhaitez commencer sans configuration, [cliquez ici pour démarrer la conversation immédiatement 🚀](${SAAS_CHAT_UTM_URL})
\\ 2 Si vous souhaitez utiliser vos propres ressources OpenAI, cliquez [ici](/#/settings) pour modifier les paramètres `
: `😆 La conversation a rencontré quelques problèmes, pas de panique :
\ 1 Si vous souhaitez commencer sans configuration, [cliquez ici pour démarrer la conversation immédiatement 🚀](${SAAS_CHAT_UTM_URL})
\ 2 Si vous utilisez une version déployée privée, cliquez [ici](/#/auth) pour entrer la clé d'accès 🔑
\ 3 Si vous souhaitez utiliser vos propres ressources OpenAI, cliquez [ici](/#/settings) pour modifier les paramètres
`,
},
Auth: {
Title: "Mot de passe requis",
Tips: "L'administrateur a activé la vérification par mot de passe. Veuillez entrer le code d'accès ci-dessous",
SubTips: "Ou entrez votre clé API OpenAI ou Google",
Input: "Entrez le code d'accès ici",
Confirm: "Confirmer",
Later: "Plus tard",
Return: "Retour",
SaasTips:
"La configuration est trop compliquée, je veux l'utiliser immédiatement",
TopTips:
"🥳 Offre de lancement NextChat AI, débloquez OpenAI o1, GPT-4o, Claude-3.5 et les derniers grands modèles",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} messages en total`, ChatItemCount: (count: number) => `${count} conversations`,
}, },
Chat: { Chat: {
SubTitle: (count: number) => `${count} messages échangés avec ChatGPT`, SubTitle: (count: number) => `Total de ${count} conversations`,
EditMessage: {
Title: "Modifier l'historique des messages",
Topic: {
Title: "Sujet de la discussion",
SubTitle: "Modifier le sujet de la discussion actuel",
},
},
Actions: { Actions: {
ChatList: "Aller à la liste de discussion", ChatList: "Voir la liste des messages",
CompressedHistory: "Mémoire d'historique compressée Prompt", CompressedHistory: "Voir l'historique des prompts compressés",
Export: "Exporter tous les messages en tant que Markdown", Export: "Exporter l'historique de la discussion",
Copy: "Copier", Copy: "Copier",
Stop: "Arrêter", Stop: "Arrêter",
Retry: "Réessayer", Retry: "Réessayer",
Delete: "Supprimer",
Pin: "Épingler", Pin: "Épingler",
PinToastContent: "Épingler 2 messages à des messages contextuels", PinToastContent: "1 conversation épinglée aux prompts prédéfinis",
PinToastAction: "Voir", PinToastAction: "Voir",
Delete: "Supprimer",
Edit: "Modifier", Edit: "Modifier",
RefreshTitle: "Actualiser le titre",
RefreshToast: "Demande d'actualisation du titre envoyée",
}, },
Commands: { Commands: {
new: "Commencer une nouvelle conversation", new: "Nouvelle discussion",
newm: "Démarrer une nouvelle conversation avec un assistant", newm: "Créer une discussion à partir du masque",
next: "Conversation suivante", next: "Discussion suivante",
prev: "Conversation précédente", prev: "Discussion précédente",
clear: "Effacer le contexte", clear: "Effacer le contexte",
del: "Supprimer la Conversation", del: "Supprimer la discussion",
}, },
InputActions: { InputActions: {
Stop: "Stop", Stop: "Arrêter la réponse",
ToBottom: "Au dernier", ToBottom: "Aller au plus récent",
Theme: { Theme: {
auto: "Auto", auto: "Thème automatique",
light: "Thème clair", light: "Mode clair",
dark: "Thème sombre", dark: "Mode sombre",
}, },
Prompt: "Instructions", Prompt: "Commandes rapides",
Masks: "Assistants", Masks: "Tous les masques",
Clear: "Effacer le contexte", Clear: "Effacer la discussion",
Settings: "Réglages", Settings: "Paramètres de la discussion",
UploadImage: "Télécharger une image",
}, },
Rename: "Renommer la conversation", Rename: "Renommer la discussion",
Typing: "En train d'écrire…", Typing: "En train d'écrire…",
Input: (submitKey: string) => { Input: (submitKey: string) => {
var inputHints = `Appuyez sur ${submitKey} pour envoyer`; var inputHints = `${submitKey} pour envoyer`;
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter pour insérer un saut de ligne"; inputHints += "Shift + Enter pour passer à la ligne";
} }
return inputHints + ", / pour rechercher des prompts"; return inputHints + "/ pour compléter, : pour déclencher des commandes";
}, },
Send: "Envoyer", Send: "Envoyer",
Config: { Config: {
Reset: "Restaurer les paramètres par défaut", Reset: "Effacer la mémoire",
SaveAs: "Enregistrer en tant que masque", SaveAs: "Enregistrer comme masque",
}, },
IsContext: "Prompt prédéfini",
}, },
Export: { Export: {
Title: "Tous les messages", Title: "Partager l'historique des discussions",
Copy: "Tout sélectionner", Copy: "Tout copier",
Download: "Télécharger", Download: "Télécharger le fichier",
MessageFromYou: "Message de votre part", Share: "Partager sur ShareGPT",
MessageFromChatGPT: "Message de ChatGPT", MessageFromYou: "Utilisateur",
MessageFromChatGPT: "ChatGPT",
Format: {
Title: "Format d'exportation",
SubTitle: "Vous pouvez exporter en texte Markdown ou en image PNG",
},
IncludeContext: {
Title: "Inclure le contexte du masque",
SubTitle: "Afficher le contexte du masque dans les messages",
},
Steps: {
Select: "Sélectionner",
Preview: "Aperçu",
},
Image: {
Toast: "Génération de la capture d'écran",
Modal:
"Appuyez longuement ou faites un clic droit pour enregistrer l'image",
},
},
Select: {
Search: "Rechercher des messages",
All: "Tout sélectionner",
Latest: "Derniers messages",
Clear: "Effacer la sélection",
}, },
Memory: { Memory: {
Title: "Prompt mémoire", Title: "Résumé historique",
EmptyContent: "Rien encore.", EmptyContent: "Le contenu de la discussion est trop court pour être résumé",
Send: "Envoyer la mémoire", Send: "Compresser automatiquement l'historique des discussions et l'envoyer comme contexte",
Copy: "Copier la mémoire", Copy: "Copier le résumé",
Reset: "Réinitialiser la session", Reset: "[unused]",
ResetConfirm: ResetConfirm: "Confirmer la suppression du résumé historique ?",
"La réinitialisation supprimera l'historique de la conversation actuelle ainsi que la mémoire de l'historique. Êtes-vous sûr de vouloir procéder à la réinitialisation?",
}, },
Home: { Home: {
NewChat: "Nouvelle discussion", NewChat: "Nouvelle discussion",
DeleteChat: "Confirmer la suppression de la conversation sélectionnée ?", DeleteChat: "Confirmer la suppression de la discussion sélectionnée ?",
DeleteToast: "Conversation supprimée", DeleteToast: "Discussion supprimée",
Revert: "Revenir en arrière", Revert: "Annuler",
}, },
Settings: { Settings: {
Title: "Paramètres", Title: "Paramètres",
SubTitle: "Toutes les configurations", SubTitle: "Toutes les options de configuration",
Danger: { Danger: {
Reset: { Reset: {
Title: "Restaurer les paramètres", Title: "Réinitialiser tous les paramètres",
SubTitle: "Restaurer les paramètres par défaut", SubTitle:
Action: "Reinitialiser", "Réinitialiser toutes les options de configuration aux valeurs par défaut",
Confirm: "Confirmer la réinitialisation des paramètres?", Action: "Réinitialiser maintenant",
Confirm: "Confirmer la réinitialisation de tous les paramètres ?",
}, },
Clear: { Clear: {
Title: "Supprimer toutes les données", Title: "Effacer toutes les données",
SubTitle: SubTitle:
"Effacer toutes les données, y compris les conversations et les paramètres", "Effacer toutes les discussions et les données de configuration",
Action: "Supprimer", Action: "Effacer maintenant",
Confirm: "Confirmer la suppression de toutes les données?", Confirm:
"Confirmer l'effacement de toutes les discussions et données de configuration ?",
}, },
}, },
Lang: { Lang: {
Name: "Language", // ATTENTION : si vous souhaitez ajouter une nouvelle traduction, ne traduisez pas cette valeur, laissez-la sous forme de `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Toutes les langues", All: "Toutes les langues",
}, },
Avatar: "Avatar", Avatar: "Avatar",
FontSize: { FontSize: {
Title: "Taille des polices", Title: "Taille de la police",
SubTitle: "Ajuste la taille de police du contenu de la conversation", SubTitle: "Taille de la police pour le contenu des discussions",
},
FontFamily: {
Title: "Police de Chat",
SubTitle:
"Police du contenu du chat, laissez vide pour appliquer la police par défaut globale",
Placeholder: "Nom de la Police",
}, },
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "Injecter des invites système", Title: "Injecter des invites système",
SubTitle: SubTitle:
"Ajoute de force une invite système simulée de ChatGPT au début de la liste des messages pour chaque demande", "Ajouter de manière forcée une invite système simulée de ChatGPT au début de chaque liste de messages",
}, },
InputTemplate: { InputTemplate: {
Title: "Template", Title: "Prétraitement des entrées utilisateur",
SubTitle: "Le message le plus récent sera ajouté à ce template.", SubTitle:
"Le dernier message de l'utilisateur sera intégré dans ce modèle",
}, },
Update: { Update: {
Version: (x: string) => `Version : ${x}`, Version: (x: string) => `Version actuelle : ${x}`,
IsLatest: "Dernière version", IsLatest: "Vous avez la dernière version",
CheckUpdate: "Vérifier la mise à jour", CheckUpdate: "Vérifier les mises à jour",
IsChecking: "Vérification de la mise à jour...", IsChecking: "Vérification des mises à jour en cours...",
FoundUpdate: (x: string) => `Nouvelle version disponible : ${x}`, FoundUpdate: (x: string) => `Nouvelle version trouvée : ${x}`,
GoToUpdate: "Mise à jour", GoToUpdate: "Aller à la mise à jour",
}, },
SendKey: "Clé d'envoi", SendKey: "Touche d'envoi",
Theme: "Thème", Theme: "Thème",
TightBorder: "Bordure serrée", TightBorder: "Mode sans bordure",
SendPreviewBubble: { SendPreviewBubble: {
Title: "Aperçu de l'envoi dans une bulle", Title: "Bulle d'aperçu",
SubTitle: "Aperçu du Markdown dans une bulle", SubTitle: "Aperçu du contenu Markdown dans la bulle d'aperçu",
},
AutoGenerateTitle: {
Title: "Génération automatique de titres",
SubTitle:
"Générer un titre approprié en fonction du contenu de la discussion",
},
Sync: {
CloudState: "Données cloud",
NotSyncYet: "Pas encore synchronisé",
Success: "Synchronisation réussie",
Fail: "Échec de la synchronisation",
Config: {
Modal: {
Title: "Configurer la synchronisation cloud",
Check: "Vérifier la disponibilité",
},
SyncType: {
Title: "Type de synchronisation",
SubTitle: "Choisissez le serveur de synchronisation préféré",
},
Proxy: {
Title: "Activer le proxy",
SubTitle:
"Lors de la synchronisation dans le navigateur, le proxy doit être activé pour éviter les restrictions de domaine croisé",
},
ProxyUrl: {
Title: "Adresse du proxy",
SubTitle:
"Uniquement pour le proxy de domaine croisé fourni par le projet",
},
WebDav: {
Endpoint: "Adresse WebDAV",
UserName: "Nom d'utilisateur",
Password: "Mot de passe",
},
UpStash: {
Endpoint: "URL REST Redis UpStash",
UserName: "Nom de sauvegarde",
Password: "Token REST Redis UpStash",
},
},
LocalState: "Données locales",
Overview: (overview: any) => {
return `${overview.chat} discussions, ${overview.message} messages, ${overview.prompt} invites, ${overview.mask} masques`;
},
ImportFailed: "Échec de l'importation",
}, },
Mask: { Mask: {
Splash: { Splash: {
Title: "Écran de masque", Title: "Page de démarrage du masque",
SubTitle: SubTitle:
"Afficher un écran de masque avant de démarrer une nouvelle discussion", "Afficher la page de démarrage du masque lors de la création d'une nouvelle discussion",
}, },
Builtin: { Builtin: {
Title: "Masquer Les Assistants Intégrés", Title: "Masquer les masques intégrés",
SubTitle: "Masquer les assistants intégrés par défaut", SubTitle:
"Masquer les masques intégrés dans toutes les listes de masques",
}, },
}, },
Prompt: { Prompt: {
Disable: { Disable: {
Title: "Désactiver la saisie semi-automatique", Title: "Désactiver la complétion automatique des invites",
SubTitle: "Appuyez sur / pour activer la saisie semi-automatique", SubTitle:
"Saisir / au début de la zone de texte pour déclencher la complétion automatique",
}, },
List: "Liste de prompts", List: "Liste des invites personnalisées",
ListCount: (builtin: number, custom: number) => ListCount: (builtin: number, custom: number) =>
`${builtin} intégré, ${custom} personnalisé`, `${builtin} intégrées, ${custom} définies par l'utilisateur`,
Edit: "Modifier", Edit: "Modifier",
Modal: { Modal: {
Title: "Liste de prompts", Title: "Liste des invites",
Add: "Ajouter un élément", Add: "Créer",
Search: "Rechercher des prompts", Search: "Rechercher des invites",
}, },
EditModal: { EditModal: {
Title: "Modifier le prompt", Title: "Modifier les invites",
}, },
}, },
HistoryCount: { HistoryCount: {
Title: "Nombre de messages joints", Title: "Nombre de messages historiques",
SubTitle: "Nombre de messages envoyés attachés par demande", SubTitle: "Nombre de messages historiques envoyés avec chaque demande",
}, },
CompressThreshold: { CompressThreshold: {
Title: "Seuil de compression de l'historique", Title: "Seuil de compression des messages historiques",
SubTitle: SubTitle:
"Comprimera si la longueur des messages non compressés dépasse cette valeur", "Compresser les messages historiques lorsque leur longueur dépasse cette valeur",
}, },
Usage: { Usage: {
Title: "Solde du compte", Title: "Vérification du solde",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
return `Épuisé ce mois-ci $${used}, abonnement $${total}`; return `Utilisé ce mois-ci : $${used}, Total d'abonnement : $${total}`;
},
IsChecking: "Vérification en cours…",
Check: "Re-vérifier",
NoAccess:
"Entrez la clé API ou le mot de passe d'accès pour vérifier le solde",
},
Access: {
SaasStart: {
Title: "Utiliser NextChat AI",
Label: "(La solution la plus rentable)",
SubTitle:
"Officiellement maintenu par NextChat, prêt à l'emploi sans configuration, prend en charge les derniers grands modèles comme OpenAI o1, GPT-4o et Claude-3.5",
ChatNow: "Discuter maintenant",
},
AccessCode: {
Title: "Mot de passe d'accès",
SubTitle: "L'administrateur a activé l'accès sécurisé",
Placeholder: "Veuillez entrer le mot de passe d'accès",
},
CustomEndpoint: {
Title: "Interface personnalisée",
SubTitle: "Utiliser un service Azure ou OpenAI personnalisé",
},
Provider: {
Title: "Fournisseur de modèle",
SubTitle: "Changer de fournisseur de service",
},
OpenAI: {
ApiKey: {
Title: "Clé API",
SubTitle:
"Utiliser une clé OpenAI personnalisée pour contourner les restrictions d'accès par mot de passe",
Placeholder: "Clé API OpenAI",
},
Endpoint: {
Title: "Adresse de l'interface",
SubTitle: "Doit inclure http(s):// en dehors de l'adresse par défaut",
},
},
Azure: {
ApiKey: {
Title: "Clé d'interface",
SubTitle:
"Utiliser une clé Azure personnalisée pour contourner les restrictions d'accès par mot de passe",
Placeholder: "Clé API Azure",
},
Endpoint: {
Title: "Adresse de l'interface",
SubTitle: "Exemple :",
},
ApiVerion: {
Title: "Version de l'interface (version API azure)",
SubTitle: "Choisissez une version spécifique",
},
},
Anthropic: {
ApiKey: {
Title: "Clé d'interface",
SubTitle:
"Utiliser une clé Anthropic personnalisée pour contourner les restrictions d'accès par mot de passe",
Placeholder: "Clé API Anthropic",
},
Endpoint: {
Title: "Adresse de l'interface",
SubTitle: "Exemple :",
},
ApiVerion: {
Title: "Version de l'interface (version API claude)",
SubTitle: "Choisissez une version spécifique de l'API",
},
},
Google: {
ApiKey: {
Title: "Clé API",
SubTitle: "Obtenez votre clé API Google AI",
Placeholder: "Entrez votre clé API Google AI Studio",
},
Endpoint: {
Title: "Adresse de l'interface",
SubTitle: "Exemple :",
},
ApiVersion: {
Title: "Version de l'API (pour gemini-pro uniquement)",
SubTitle: "Choisissez une version spécifique de l'API",
},
GoogleSafetySettings: {
Title: "Niveau de filtrage de sécurité Google",
SubTitle: "Définir le niveau de filtrage du contenu",
},
},
Baidu: {
ApiKey: {
Title: "Clé API",
SubTitle: "Utiliser une clé API Baidu personnalisée",
Placeholder: "Clé API Baidu",
},
SecretKey: {
Title: "Clé secrète",
SubTitle: "Utiliser une clé secrète Baidu personnalisée",
Placeholder: "Clé secrète Baidu",
},
Endpoint: {
Title: "Adresse de l'interface",
SubTitle:
"Non pris en charge pour les configurations personnalisées dans .env",
},
},
ByteDance: {
ApiKey: {
Title: "Clé d'interface",
SubTitle: "Utiliser une clé API ByteDance personnalisée",
Placeholder: "Clé API ByteDance",
},
Endpoint: {
Title: "Adresse de l'interface",
SubTitle: "Exemple :",
},
},
Alibaba: {
ApiKey: {
Title: "Clé d'interface",
SubTitle: "Utiliser une clé API Alibaba Cloud personnalisée",
Placeholder: "Clé API Alibaba Cloud",
},
Endpoint: {
Title: "Adresse de l'interface",
SubTitle: "Exemple :",
},
},
CustomModel: {
Title: "Nom du modèle personnalisé",
SubTitle:
"Ajouter des options de modèles personnalisés, séparées par des virgules",
}, },
IsChecking: "Vérification...",
Check: "Vérifier",
NoAccess: "Entrez la clé API pour vérifier le solde",
}, },
Model: "Modèle", Model: "Modèle",
CompressModel: {
Title: "Modèle de compression",
SubTitle: "Modèle utilisé pour compresser l'historique",
},
Temperature: { Temperature: {
Title: "Température", Title: "Aléatoire (temperature)",
SubTitle: "Une valeur plus élevée rendra les réponses plus aléatoires", SubTitle: "Plus la valeur est élevée, plus les réponses sont aléatoires",
}, },
TopP: { TopP: {
Title: "Top P", Title: "Échantillonnage par noyau (top_p)",
SubTitle: SubTitle:
"Ne modifiez pas à moins que vous ne sachiez ce que vous faites", "Semblable à l'aléatoire, mais ne pas modifier en même temps que l'aléatoire",
}, },
MaxTokens: { MaxTokens: {
Title: "Limite de Tokens", Title: "Limite de réponse unique (max_tokens)",
SubTitle: "Longueur maximale des tokens d'entrée et des tokens générés", SubTitle: "Nombre maximal de tokens utilisés pour une interaction unique",
}, },
PresencePenalty: { PresencePenalty: {
Title: "Pénalité de présence", Title: "Nouveauté du sujet (presence_penalty)",
SubTitle: SubTitle:
"Une valeur plus élevée augmentera la probabilité d'introduire de nouveaux sujets", "Plus la valeur est élevée, plus il est probable d'élargir aux nouveaux sujets",
}, },
FrequencyPenalty: { FrequencyPenalty: {
Title: "Pénalité de fréquence", Title: "Pénalité de fréquence (frequency_penalty)",
SubTitle: SubTitle:
"Une valeur plus élevée diminuant la probabilité de répéter la même ligne", "Plus la valeur est élevée, plus il est probable de réduire les répétitions",
}, },
}, },
Store: { Store: {
DefaultTopic: "Nouvelle conversation", DefaultTopic: "Nouvelle discussion",
BotHello: "Bonjour ! Comment puis-je vous aider aujourd'hui ?", BotHello: "Comment puis-je vous aider ?",
Error: "Quelque chose s'est mal passé, veuillez réessayer plus tard.", Error: "Une erreur est survenue, veuillez réessayer plus tard",
Prompt: { Prompt: {
History: (content: string) => History: (content: string) =>
"Ceci est un résumé de l'historique des discussions entre l'IA et l'utilisateur : " + "Voici le résumé de la discussion précédente : " + content,
content,
Topic: Topic:
"Veuillez générer un titre de quatre à cinq mots résumant notre conversation sans introduction, ponctuation, guillemets, points, symboles ou texte supplémentaire. Supprimez les guillemets inclus.", "Utilisez quatre à cinq mots pour retourner le sujet succinct de cette phrase, sans explication, sans ponctuation, sans interjections, sans texte superflu, sans gras. Si aucun sujet, retournez simplement « discussion informelle »",
Summarize: Summarize:
"Résumez brièvement nos discussions en 200 mots ou moins pour les utiliser comme prompt de contexte futur.", "Faites un résumé succinct de la discussion, à utiliser comme prompt de contexte ultérieur, en moins de 200 mots",
}, },
}, },
Copy: { Copy: {
Success: "Copié dans le presse-papiers", Success: "Copié dans le presse-papiers",
Failed: Failed: "Échec de la copie, veuillez autoriser l'accès au presse-papiers",
"La copie a échoué, veuillez accorder l'autorisation d'accès au presse-papiers", },
Download: {
Success: "Le contenu a été téléchargé dans votre répertoire.",
Failed: "Échec du téléchargement.",
}, },
Context: { Context: {
Toast: (x: any) => `Avec ${x} contextes de prompts`, Toast: (x: any) => `Contient ${x} invites prédéfinies`,
Edit: "Contextes et mémoires de prompts", Edit: "Paramètres de la discussion actuelle",
Add: "Ajouter un prompt", Add: "Ajouter une discussion",
Clear: "Contexte effacé",
Revert: "Restaurer le contexte",
}, },
Plugin: { Plugin: {
Name: "Extension", Name: "Plugin",
}, },
FineTuned: { FineTuned: {
Sysmessage: "Eres un asistente que", Sysmessage: "Vous êtes un assistant",
},
SearchChat: {
Name: "Recherche",
Page: {
Title: "Rechercher dans l'historique des discussions",
Search: "Entrez le mot-clé de recherche",
NoResult: "Aucun résultat trouvé",
NoData: "Aucune donnée",
Loading: "Chargement",
SubTitle: (count: number) => `${count} résultats trouvés`,
},
Item: {
View: "Voir",
},
}, },
Mask: { Mask: {
Name: "Masque", Name: "Masque",
Page: { Page: {
Title: "Modèle de prompt", Title: "Masques de rôle prédéfinis",
SubTitle: (count: number) => `${count} modèles de prompts`, SubTitle: (count: number) => `${count} définitions de rôle prédéfinies`,
Search: "Rechercher des modèles", Search: "Rechercher des masques de rôle",
Create: "Créer", Create: "Créer",
}, },
Item: { Item: {
Info: (count: number) => `${count} prompts`, Info: (count: number) => `Contient ${count} discussions prédéfinies`,
Chat: "Discussion", Chat: "Discussion",
View: "Vue", View: "Voir",
Edit: "Modifier", Edit: "Modifier",
Delete: "Supprimer", Delete: "Supprimer",
DeleteConfirm: "Confirmer la suppression?", DeleteConfirm: "Confirmer la suppression ?",
}, },
EditModal: { EditModal: {
Title: (readonly: boolean) => Title: (readonly: boolean) =>
`Modifier le modèle de prompt ${readonly ? "(en lecture seule)" : ""}`, `Modifier le masque prédéfini ${readonly ? " (lecture seule)" : ""}`,
Download: "Télécharger", Download: "Télécharger le masque",
Clone: "Dupliquer", Clone: "Cloner le masque",
}, },
Config: { Config: {
Avatar: "Avatar de lassistant", Avatar: "Avatar du rôle",
Name: "Nom de lassistant", Name: "Nom du rôle",
Sync: { Sync: {
Title: "Utiliser la configuration globale", Title: "Utiliser les paramètres globaux",
SubTitle: "Utiliser la configuration globale dans cette conversation", SubTitle:
Confirm: "Voulez-vous definir votre configuration personnalisée ?", "Cette discussion utilise-t-elle les paramètres du modèle globaux ?",
Confirm:
"Les paramètres personnalisés de cette discussion seront automatiquement remplacés. Confirmer l'activation des paramètres globaux ?",
}, },
HideContext: { HideContext: {
Title: "Masquer les invites contextuelles", Title: "Masquer les discussions prédéfinies",
SubTitle: "Ne pas afficher les instructions contextuelles dans le chat", SubTitle:
"Les discussions prédéfinies ne seront pas affichées dans l'interface de discussion après masquage",
}, },
Share: { Share: {
Title: "Partager ce masque", Title: "Partager ce masque",
SubTitle: "Générer un lien vers ce masque", SubTitle: "Générer un lien direct pour ce masque",
Action: "Copier le lien", Action: "Copier le lien",
}, },
}, },
}, },
NewChat: { NewChat: {
Return: "Retour", Return: "Retour",
Skip: "Passer", Skip: "Commencer directement",
Title: "Choisir un assitant", NotShow: "Ne plus afficher",
SubTitle: "Discutez avec l'âme derrière le masque",
More: "En savoir plus",
NotShow: "Ne pas afficher à nouveau",
ConfirmNoShow: ConfirmNoShow:
"Confirmez-vous vouloir désactiver cela? Vous pouvez le réactiver plus tard dans les paramètres.", "Confirmer la désactivation ? Vous pourrez réactiver cette option à tout moment dans les paramètres.",
Title: "Choisir un masque",
SubTitle: "Commencez maintenant, rencontrez les pensées derrière le masque",
More: "Voir tout",
},
URLCommand: {
Code: "Code d'accès détecté dans le lien, souhaitez-vous le remplir automatiquement ?",
Settings:
"Paramètres prédéfinis détectés dans le lien, souhaitez-vous les remplir automatiquement ?",
}, },
UI: { UI: {
@ -296,9 +586,17 @@ const fr: PartialLocaleType = {
Cancel: "Annuler", Cancel: "Annuler",
Close: "Fermer", Close: "Fermer",
Create: "Créer", Create: "Créer",
Edit: "Éditer", Edit: "Modifier",
Export: "Exporter",
Import: "Importer",
Sync: "Synchroniser",
Config: "Configurer",
}, },
Exporter: { Exporter: {
Description: {
Title:
"Seuls les messages après avoir effacé le contexte seront affichés",
},
Model: "Modèle", Model: "Modèle",
Messages: "Messages", Messages: "Messages",
Topic: "Sujet", Topic: "Sujet",

Some files were not shown because too many files have changed in this diff Show More