mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-31 03:09:04 +08:00
Compare commits
124 Commits
add_tip
...
update-max
Author | SHA1 | Date | |
---|---|---|---|
|
ebe617b733 | ||
|
c139038e01 | ||
|
4a7fd3a380 | ||
|
c98dc31cdf | ||
|
c5074f0aa4 | ||
|
ba58018a15 | ||
|
63ab83c3c8 | ||
|
268cf3b606 | ||
|
fbc68fa776 | ||
|
4ae34ea3ee | ||
|
96273fd75e | ||
|
3e63d405c1 | ||
|
19b42aac5d | ||
|
b67a23200e | ||
|
1dac02e4d6 | ||
|
acad5b1d08 | ||
|
4e9bb51d2f | ||
|
c0c8cdbbf3 | ||
|
cbdc611b54 | ||
|
93ca303b6c | ||
|
a925b424a8 | ||
|
5b4d423b58 | ||
|
6c1cbe120c | ||
|
77a58bc4b0 | ||
|
8ad63a6c25 | ||
|
acf9fa36f9 | ||
|
461154bb03 | ||
|
cd75461f9e | ||
|
2bac174e6f | ||
|
65f80f81ad | ||
|
450766a44b | ||
|
05e6e4bffb | ||
|
fbb66a4a5d | ||
|
d51d31a559 | ||
|
919ee51dca | ||
|
9c577ad9d5 | ||
|
953114041b | ||
|
d830c23dab | ||
|
fd3568c459 | ||
|
3029dcb2f6 | ||
|
35e03e1bca | ||
|
cea5b91f96 | ||
|
d2984db6e7 | ||
|
deb215ccd1 | ||
|
7173cf2184 | ||
|
0c697e123d | ||
|
edfa6d14ee | ||
|
b6d9ba93fa | ||
|
6293b95a3b | ||
|
ef4665cd8b | ||
|
8030e71a5a | ||
|
f42488d4cb | ||
|
af49ed4fdc | ||
|
b174a40634 | ||
|
3c01738c29 | ||
|
9be58f3eb4 | ||
|
a50c282d01 | ||
|
5141145e4d | ||
|
b5f6e5a598 | ||
|
7df308d655 | ||
|
f5ad51a35e | ||
|
f9d4105170 | ||
|
9e6ee50fa6 | ||
|
dd77ad5d74 | ||
|
3898c507c4 | ||
|
fcba50f041 | ||
|
452fc86ad1 | ||
|
5bdf411399 | ||
|
2d920f7ccc | ||
|
d84d51b475 | ||
|
f9d6f4f9da | ||
|
a13bd624e8 | ||
|
8fb019b2e2 | ||
|
2f3457e73d | ||
|
c6ebd6e73c | ||
|
2333a47c55 | ||
|
b35895b551 | ||
|
19c4ed4463 | ||
|
22aa1698b4 | ||
|
07d089a2bd | ||
|
870ad913cc | ||
|
3fb389551b | ||
|
d12a4adfb5 | ||
|
702e17c96b | ||
|
93ff7d26cc | ||
|
13777786c4 | ||
|
6655c64e55 | ||
|
7f3f6f1aaf | ||
|
ea04595c5e | ||
|
13c68bd810 | ||
|
1d2f44fba8 | ||
|
68702bfb1f | ||
|
b7892b58f5 | ||
|
4c84182e7a | ||
|
e8581c8f3c | ||
|
9bbd7d3185 | ||
|
dbabb2c403 | ||
|
6c37d04591 | ||
|
649c5be64e | ||
|
fc0042a799 | ||
|
269d064e0a | ||
|
6c8143b7de | ||
|
f9f99639db | ||
|
46fc2a5012 | ||
|
90e7b5aecf | ||
|
ed20fd2962 | ||
|
4c3fd55a75 | ||
|
d95d509046 | ||
|
4a60512ae7 | ||
|
0e210cf8de | ||
|
35aa2c7270 | ||
|
23f2b6213c | ||
|
3a969054e3 | ||
|
4d1f9e49d4 | ||
|
2474d5b6d2 | ||
|
c75d9e3de4 | ||
|
df222ded12 | ||
|
212d15fdd0 | ||
|
b5ba05dd83 | ||
|
accb526cd6 | ||
|
56eb9d1430 | ||
|
1287e39cc6 | ||
|
1ef2aa35e9 | ||
|
4d6b981a54 |
@@ -66,4 +66,4 @@ ANTHROPIC_API_VERSION=
|
||||
ANTHROPIC_URL=
|
||||
|
||||
### (optional)
|
||||
WHITE_WEBDEV_ENDPOINTS=
|
||||
WHITE_WEBDAV_ENDPOINTS=
|
21
README.md
21
README.md
@@ -12,15 +12,18 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
||||
|
||||
一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。
|
||||
|
||||
[![Saas][Saas-image]][saas-url]
|
||||
[![Web][Web-image]][web-url]
|
||||
[![Windows][Windows-image]][download-url]
|
||||
[![MacOS][MacOS-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) / [Enterprise Edition](#enterprise-edition) / [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) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](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/
|
||||
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
|
||||
[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
|
||||
@@ -60,7 +63,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
|
||||
|
||||
企业版咨询: **business@nextchat.dev**
|
||||
|
||||
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
|
||||
<img width="300" src="https://github.com/user-attachments/assets/3d4305ac-6e95-489e-884b-51d51db5f692">
|
||||
|
||||
## Features
|
||||
|
||||
@@ -97,6 +100,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
|
||||
|
||||
## 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.
|
||||
@@ -134,6 +138,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
|
||||
|
||||
## 最新动态
|
||||
|
||||
- 🚀 v2.15.4 客户端支持Tauri本地直接调用大模型API,更安全
|
||||
- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
|
||||
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
|
||||
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
|
||||
@@ -172,7 +177,7 @@ We recommend that you follow the steps below to re-deploy:
|
||||
|
||||
### 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:
|
||||
|
||||
@@ -329,9 +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.
|
||||
|
||||
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.
|
||||
> If you only can use Azure model, `-all,+gpt-3.5-turbo@azure=gpt35` will `gpt35(Azure)` the only option in model list.
|
||||
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.
|
||||
> 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.
|
||||
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
|
||||
@@ -340,7 +345,7 @@ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name
|
||||
|
||||
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:
|
||||
- Each address must be a complete endpoint
|
||||
|
12
README_CN.md
12
README_CN.md
@@ -8,7 +8,7 @@
|
||||
|
||||
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
|
||||
|
||||
[企业版](#%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)
|
||||
[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)
|
||||
|
||||
[<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)
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
### 打开自动更新
|
||||
|
||||
> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次!
|
||||
> 如果你遇到了 Upstream Sync 执行错误,请[手动 Sync Fork 一次](./README_CN.md#手动更新代码)!
|
||||
|
||||
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
|
||||
|
||||
@@ -202,7 +202,7 @@ ByteDance Api Url.
|
||||
|
||||
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
|
||||
|
||||
### `WHITE_WEBDEV_ENDPOINTS` (可选)
|
||||
### `WHITE_WEBDAV_ENDPOINTS` (可选)
|
||||
|
||||
如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
|
||||
- 每一个地址必须是一个完整的 endpoint
|
||||
@@ -216,9 +216,9 @@ ByteDance Api Url.
|
||||
|
||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||
|
||||
在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
||||
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
|
||||
> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
|
||||
在Azure的模式下,支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
||||
> 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
|
||||
> 如果你只能使用Azure模式,那么设置 `-all,+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)`的选项
|
||||
|
10
README_JA.md
10
README_JA.md
@@ -5,7 +5,7 @@
|
||||
|
||||
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
|
||||
|
||||
[企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
|
||||
[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)
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
### 自動更新を開く
|
||||
|
||||
> Upstream Sync の実行エラーが発生した場合は、手動で Sync Fork してください!
|
||||
> Upstream Sync の実行エラーが発生した場合は、[手動で Sync Fork](./README_JA.md#手動でコードを更新する) してください!
|
||||
|
||||
プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
|
||||
|
||||
@@ -193,7 +193,7 @@ ByteDance API の URL。
|
||||
|
||||
リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
|
||||
|
||||
### `WHITE_WEBDEV_ENDPOINTS` (オプション)
|
||||
### `WHITE_WEBDAV_ENDPOINTS` (オプション)
|
||||
|
||||
アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
|
||||
- 各アドレスは完全なエンドポイントでなければなりません。
|
||||
@@ -207,8 +207,8 @@ ByteDance API の URL。
|
||||
|
||||
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
|
||||
|
||||
Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
||||
> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
|
||||
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)` のオプションが表示されます。
|
||||
|
@@ -23,7 +23,8 @@ export 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 apiKey = token ? token : serverConfig.googleApiKey;
|
||||
@@ -91,8 +92,8 @@ async function request(req: NextRequest, apiKey: string) {
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
const fetchUrl = `${baseUrl}${path}?key=${apiKey}${
|
||||
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "&alt=sse" : ""
|
||||
const fetchUrl = `${baseUrl}${path}${
|
||||
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : ""
|
||||
}`;
|
||||
|
||||
console.log("[Fetch Url] ", fetchUrl);
|
||||
@@ -100,6 +101,9 @@ async function request(req: NextRequest, apiKey: string) {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
"x-goog-api-key":
|
||||
req.headers.get("x-goog-api-key") ||
|
||||
(req.headers.get("Authorization") ?? "").replace("Bearer ", ""),
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
|
@@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "./auth";
|
||||
import { requestOpenai } from "./common";
|
||||
|
||||
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
|
||||
const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
|
||||
|
||||
function getModels(remoteModelRes: OpenAIListModelResponse) {
|
||||
const config = getServerSideConfig();
|
||||
@@ -34,7 +34,7 @@ export async function handle(
|
||||
|
||||
const subpath = params.path.join("/");
|
||||
|
||||
if (!ALLOWD_PATH.has(subpath)) {
|
||||
if (!ALLOWED_PATH.has(subpath)) {
|
||||
console.log("[OpenAI Route] forbidden path ", subpath);
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
@@ -6,7 +6,7 @@ const config = getServerSideConfig();
|
||||
|
||||
const mergedAllowedWebDavEndpoints = [
|
||||
...internalAllowedWebDavEndpoints,
|
||||
...config.allowedWebDevEndpoints,
|
||||
...config.allowedWebDavEndpoints,
|
||||
].filter((domain) => Boolean(domain.trim()));
|
||||
|
||||
const normalizeUrl = (url: string) => {
|
||||
|
@@ -231,7 +231,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
|
||||
function getConfig() {
|
||||
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 isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
||||
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
|
||||
@@ -272,7 +272,13 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -283,14 +289,15 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
apiKey,
|
||||
isEnabledAccessControl,
|
||||
} = 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
|
||||
if (isBaidu && clientConfig?.isApp) return headers;
|
||||
|
||||
const authHeader = getAuthHeader();
|
||||
|
||||
const bearerToken = getBearerToken(apiKey, isAzure || isAnthropic);
|
||||
const bearerToken = getBearerToken(
|
||||
apiKey,
|
||||
isAzure || isAnthropic || isGoogle,
|
||||
);
|
||||
|
||||
if (bearerToken) {
|
||||
headers[authHeader] = bearerToken;
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -178,6 +179,7 @@ export class QwenApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
ChatMessageTool,
|
||||
} from "@/app/store";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
||||
import { ANTHROPIC_BASE_URL } from "@/app/constant";
|
||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
@@ -388,9 +388,7 @@ export class ClaudeApi implements LLMApi {
|
||||
if (baseUrl.trim().length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/anthropic"
|
||||
: ApiPath.Anthropic;
|
||||
baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
|
||||
}
|
||||
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
|
||||
|
@@ -24,6 +24,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -197,6 +198,7 @@ export class ErnieApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -165,6 +166,7 @@ export class DoubaoApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
@@ -7,21 +7,26 @@ import {
|
||||
LLMUsage,
|
||||
SpeechOptions,
|
||||
} 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 {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
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 {
|
||||
getMessageTextContent,
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
} 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 {
|
||||
path(path: string): string {
|
||||
@@ -34,7 +39,7 @@ export class GeminiProApi implements LLMApi {
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
if (baseUrl.length === 0) {
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google;
|
||||
baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
||||
}
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
@@ -48,10 +53,6 @@ export class GeminiProApi implements LLMApi {
|
||||
let chatPath = [baseUrl, path].join("/");
|
||||
|
||||
chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
|
||||
// if chatPath.startsWith('http') then add key in query string
|
||||
if (chatPath.startsWith("http") && accessStore.googleApiKey) {
|
||||
chatPath += `&key=${accessStore.googleApiKey}`;
|
||||
}
|
||||
return chatPath;
|
||||
}
|
||||
extractMessage(res: any) {
|
||||
@@ -181,114 +182,81 @@ export class GeminiProApi implements LLMApi {
|
||||
);
|
||||
|
||||
if (shouldStream) {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
chatPath,
|
||||
requestPayload,
|
||||
getHeaders(),
|
||||
// @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 = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
if (finished || controller.signal.aborted) {
|
||||
responseText += remainText;
|
||||
finish();
|
||||
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();
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log(
|
||||
"[Gemini] request response content type: ",
|
||||
contentType,
|
||||
const functionCall = chunkJson?.candidates
|
||||
?.at(0)
|
||||
?.content.parts.at(0)?.functionCall;
|
||||
if (functionCall) {
|
||||
const { name, args } = functionCall;
|
||||
runTools.push({
|
||||
id: nanoid(),
|
||||
type: "function",
|
||||
function: {
|
||||
name,
|
||||
arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
|
||||
},
|
||||
});
|
||||
}
|
||||
return chunkJson?.candidates?.at(0)?.content.parts.at(0)?.text;
|
||||
},
|
||||
// processToolMessage, include tool_calls message and tool call results
|
||||
(
|
||||
requestPayload: RequestPayload,
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// @ts-ignore
|
||||
requestPayload?.contents?.splice(
|
||||
// @ts-ignore
|
||||
requestPayload?.contents?.length,
|
||||
0,
|
||||
{
|
||||
role: "model",
|
||||
parts: toolCallMessage.tool_calls.map(
|
||||
(tool: ChatMessageTool) => ({
|
||||
functionCall: {
|
||||
name: tool?.function?.name,
|
||||
args: JSON.parse(tool?.function?.arguments as string),
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
// @ts-ignore
|
||||
...toolCallResult.map((result) => ({
|
||||
role: "function",
|
||||
parts: [
|
||||
{
|
||||
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) {
|
||||
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,
|
||||
});
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
IFLYTEK_BASE_URL,
|
||||
Iflytek,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
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";
|
||||
|
||||
@@ -40,7 +41,7 @@ export class SparkApi implements LLMApi {
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.Iflytek;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -149,6 +150,7 @@ export class SparkApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
@@ -2,7 +2,7 @@
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
MOONSHOT_BASE_URL,
|
||||
Moonshot,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
@@ -40,7 +40,7 @@ export class MoonshotApi implements LLMApi {
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.Moonshot;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
OPENAI_BASE_URL,
|
||||
DEFAULT_MODELS,
|
||||
OpenaiPath,
|
||||
Azure,
|
||||
@@ -63,7 +63,7 @@ export interface RequestPayload {
|
||||
presence_penalty: number;
|
||||
frequency_penalty: number;
|
||||
top_p: number;
|
||||
max_tokens?: number;
|
||||
max_completions_tokens?: number;
|
||||
}
|
||||
|
||||
export interface DalleRequestPayload {
|
||||
@@ -98,7 +98,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
baseUrl = isApp ? OPENAI_BASE_URL : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -228,13 +228,16 @@ export class ChatGPTApi implements LLMApi {
|
||||
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.
|
||||
// max_completions_tokens: Math.max(modelConfig.max_completions_tokens, 1024),
|
||||
// Please do not ask me why not send max_completions_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||
};
|
||||
|
||||
// add max_tokens to vision model
|
||||
// add max_completions_tokens to vision model
|
||||
if (visionModel) {
|
||||
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
|
||||
requestPayload["max_completions_tokens"] = Math.max(
|
||||
modelConfig.max_completions_tokens,
|
||||
4000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +280,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
);
|
||||
}
|
||||
if (shouldStream) {
|
||||
let index = -1;
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
@@ -302,10 +306,10 @@ export class ChatGPTApi implements LLMApi {
|
||||
}>;
|
||||
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) {
|
||||
index += 1;
|
||||
runTools.push({
|
||||
id,
|
||||
type: tool_calls[0]?.type,
|
||||
@@ -327,6 +331,8 @@ export class ChatGPTApi implements LLMApi {
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// reset index value
|
||||
index = -1;
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
|
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { ApiPath, TENCENT_BASE_URL, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ 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;
|
||||
@@ -70,9 +71,7 @@ export class HunyuanApi implements LLMApi {
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||
: ApiPath.Tencent;
|
||||
baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -179,6 +178,7 @@ export class HunyuanApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
@@ -11,6 +11,7 @@ import Logo from "../icons/logo.svg";
|
||||
import { useMobileScreen } from "@/app/utils";
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { PasswordInput } from "./ui-lib";
|
||||
import LeftIcon from "@/app/icons/left.svg";
|
||||
import { safeLocalStorage } from "@/app/utils";
|
||||
import {
|
||||
@@ -60,36 +61,43 @@ export function AuthPage() {
|
||||
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
|
||||
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
|
||||
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Auth.Input}
|
||||
<PasswordInput
|
||||
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||
aria={Locale.Settings.ShowPassword}
|
||||
aria-label={Locale.Auth.Input}
|
||||
value={accessStore.accessCode}
|
||||
type="text"
|
||||
placeholder={Locale.Auth.Input}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.accessCode = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!accessStore.hideUserApiKey ? (
|
||||
<>
|
||||
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
<PasswordInput
|
||||
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||
aria={Locale.Settings.ShowPassword}
|
||||
aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
value={accessStore.openaiApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.openaiApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className={styles["auth-input-second"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
<PasswordInput
|
||||
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||
aria={Locale.Settings.ShowPassword}
|
||||
aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
value={accessStore.googleApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.googleApiKey = e.currentTarget.value),
|
||||
|
@@ -1815,6 +1815,7 @@ function _Chat() {
|
||||
{message?.tools?.map((tool) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
title={tool?.errorMsg}
|
||||
className={styles["chat-message-tool"]}
|
||||
>
|
||||
{tool.isError === false ? (
|
||||
|
@@ -21,6 +21,9 @@ import {
|
||||
} from "./artifacts";
|
||||
import { useChatStore } from "../store";
|
||||
import { IconButton } from "./button";
|
||||
|
||||
import { useAppConfig } from "../store/config";
|
||||
|
||||
export function Mermaid(props: { code: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
@@ -91,7 +94,9 @@ export function PreCode(props: { children: any }) {
|
||||
}
|
||||
}, 600);
|
||||
|
||||
const enableArtifacts = session.mask?.enableArtifacts !== false;
|
||||
const config = useAppConfig();
|
||||
const enableArtifacts =
|
||||
session.mask?.enableArtifacts !== false && config.enableArtifacts;
|
||||
|
||||
//Wrap the paragraph for plain-text
|
||||
useEffect(() => {
|
||||
@@ -127,8 +132,9 @@ export function PreCode(props: { children: any }) {
|
||||
className="copy-code-button"
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
const code = ref.current.innerText;
|
||||
copyToClipboard(code);
|
||||
copyToClipboard(
|
||||
ref.current.querySelector("code")?.innerText ?? "",
|
||||
);
|
||||
}
|
||||
}}
|
||||
></span>
|
||||
@@ -201,23 +207,6 @@ function CustomCode(props: { children: any; className?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function escapeDollarNumber(text: string) {
|
||||
let escapedText = "";
|
||||
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
let char = text[i];
|
||||
const nextChar = text[i + 1] || " ";
|
||||
|
||||
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
|
||||
char = "\\$";
|
||||
}
|
||||
|
||||
escapedText += char;
|
||||
}
|
||||
|
||||
return escapedText;
|
||||
}
|
||||
|
||||
function escapeBrackets(text: string) {
|
||||
const pattern =
|
||||
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
|
||||
@@ -246,7 +235,7 @@ function tryWrapHtmlCode(text: string) {
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g,
|
||||
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
|
||||
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
|
||||
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
|
||||
},
|
||||
@@ -255,7 +244,7 @@ function tryWrapHtmlCode(text: string) {
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
const escapedContent = useMemo(() => {
|
||||
return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
|
||||
return tryWrapHtmlCode(escapeBrackets(props.content));
|
||||
}, [props.content]);
|
||||
|
||||
return (
|
||||
@@ -277,6 +266,20 @@ function _MarkDownContent(props: { content: string }) {
|
||||
p: (pProps) => <p {...pProps} dir="auto" />,
|
||||
a: (aProps) => {
|
||||
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 target = isInternal ? "_self" : aProps.target ?? "_blank";
|
||||
return <a {...aProps} target={target} />;
|
||||
|
@@ -166,21 +166,23 @@ export function MaskConfig(props: {
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<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.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>
|
||||
)}
|
||||
|
||||
{!props.shouldSyncFromGlobal ? (
|
||||
<ListItem
|
||||
|
@@ -10,7 +10,29 @@
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
min-width: 300px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ 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 EyeIcon from "../icons/eye.svg";
|
||||
import ConfirmIcon from "../icons/confirm.svg";
|
||||
import ReloadIcon from "../icons/reload.svg";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
@@ -29,7 +28,6 @@ import {
|
||||
import Locale from "../locales";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
export function PluginPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -209,19 +207,11 @@ export function PluginPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles["mask-actions"]}>
|
||||
{m.builtin ? (
|
||||
<IconButton
|
||||
icon={<EyeIcon />}
|
||||
text={Locale.Plugin.Item.View}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
text={Locale.Plugin.Item.Edit}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
text={Locale.Plugin.Item.Edit}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
{!m.builtin && (
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
@@ -325,30 +315,13 @@ export function PluginPage() {
|
||||
></PasswordInput>
|
||||
</ListItem>
|
||||
)}
|
||||
{!getClientConfig()?.isApp && (
|
||||
<ListItem
|
||||
title={Locale.Plugin.Auth.Proxy}
|
||||
subTitle={Locale.Plugin.Auth.ProxyDescription}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingPlugin?.usingProxy}
|
||||
style={{ minWidth: 16 }}
|
||||
onChange={(e) => {
|
||||
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||
plugin.usingProxy = e.currentTarget.checked;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
<List>
|
||||
<ListItem title={Locale.Plugin.EditModal.Content}>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<div className={pluginStyles["plugin-schema"]}>
|
||||
<input
|
||||
type="text"
|
||||
style={{ minWidth: 200, marginRight: 20 }}
|
||||
style={{ minWidth: 200 }}
|
||||
onInput={(e) => setLoadUrl(e.currentTarget.value)}
|
||||
></input>
|
||||
<IconButton
|
||||
|
@@ -1492,6 +1492,23 @@ export function Settings() {
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.Artifacts.Title}
|
||||
subTitle={Locale.Mask.Config.Artifacts.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.Artifacts.Title}
|
||||
type="checkbox"
|
||||
checked={config.enableArtifacts}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.enableArtifacts = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<SyncItems />
|
||||
|
@@ -154,8 +154,8 @@ export const getServerSideConfig = () => {
|
||||
// `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
|
||||
// );
|
||||
|
||||
const allowedWebDevEndpoints = (
|
||||
process.env.WHITE_WEBDEV_ENDPOINTS ?? ""
|
||||
const allowedWebDavEndpoints = (
|
||||
process.env.WHITE_WEBDAV_ENDPOINTS ?? ""
|
||||
).split(",");
|
||||
|
||||
return {
|
||||
@@ -229,6 +229,6 @@ export const getServerSideConfig = () => {
|
||||
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
||||
customModels,
|
||||
defaultModel,
|
||||
allowedWebDevEndpoints,
|
||||
allowedWebDavEndpoints,
|
||||
};
|
||||
};
|
||||
|
@@ -11,7 +11,6 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||
|
||||
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
||||
|
||||
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
|
||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||
|
||||
@@ -503,3 +502,4 @@ export const PLUGINS = [
|
||||
];
|
||||
|
||||
export const SAAS_CHAT_URL = "https://nextchat.dev/chat";
|
||||
export const SAAS_CHAT_UTM_URL = "https://nextchat.dev/chat?utm=github";
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const ar: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const ar: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 واجهت المحادثة بعض المشكلات، لا داعي للقلق:
|
||||
\\ 1️⃣ إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ إذا كنت تريد استخدام موارد OpenAI الخاصة بك، انقر [هنا](/#/settings) لتعديل الإعدادات ⚙️`
|
||||
: `😆 واجهت المحادثة بعض المشكلات، لا داعي للقلق:
|
||||
\ 1️⃣ إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ إذا كنت تستخدم إصدار النشر الخاص، انقر [هنا](/#/auth) لإدخال مفتاح الوصول 🔑
|
||||
\ 3️⃣ إذا كنت تريد استخدام موارد OpenAI الخاصة بك، انقر [هنا](/#/settings) لتعديل الإعدادات ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const bn: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const bn: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 কথোপকথনে কিছু সমস্যা হয়েছে, চিন্তার কিছু নেই:
|
||||
\\ 1️⃣ যদি আপনি শূন্য কনফিগারেশনে শুরু করতে চান, তাহলে [এখানে ক্লিক করে অবিলম্বে কথোপকথন শুরু করুন 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ যদি আপনি শূন্য কনফিগারেশনে শুরু করতে চান, তাহলে [এখানে ক্লিক করে অবিলম্বে কথোপকথন শুরু করুন 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ যদি আপনি আপনার নিজস্ব OpenAI সম্পদ ব্যবহার করতে চান, তাহলে [এখানে ক্লিক করুন](/#/settings) সেটিংস পরিবর্তন করতে ⚙️`
|
||||
: `😆 কথোপকথনে কিছু সমস্যা হয়েছে, চিন্তার কিছু নেই:
|
||||
\ 1️⃣ যদি আপনি শূন্য কনফিগারেশনে শুরু করতে চান, তাহলে [এখানে ক্লিক করে অবিলম্বে কথোপকথন শুরু করুন 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ যদি আপনি শূন্য কনফিগারেশনে শুরু করতে চান, তাহলে [এখানে ক্লিক করে অবিলম্বে কথোপকথন শুরু করুন 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ যদি আপনি একটি প্রাইভেট ডেপ্লয়মেন্ট সংস্করণ ব্যবহার করেন, তাহলে [এখানে ক্লিক করুন](/#/auth) প্রবেশাধিকার কীগুলি প্রবেশ করতে 🔑
|
||||
\ 3️⃣ যদি আপনি আপনার নিজস্ব OpenAI সম্পদ ব্যবহার করতে চান, তাহলে [এখানে ক্লিক করুন](/#/settings) সেটিংস পরিবর্তন করতে ⚙️
|
||||
`,
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
|
||||
const SAAS_CHAT_URL_WITH_PARAM = `${SAAS_CHAT_URL}?data=title`;
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const cn = {
|
||||
@@ -10,10 +9,10 @@ const cn = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 对话遇到了一些问题,不用慌:
|
||||
\\ 1️⃣ 想要零配置开箱即用,[点击这里立刻开启对话 🚀](${SAAS_CHAT_URL_WITH_PARAM})
|
||||
\\ 1️⃣ 想要零配置开箱即用,[点击这里立刻开启对话 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ 如果你想消耗自己的 OpenAI 资源,点击[这里](/#/settings)修改设置 ⚙️`
|
||||
: `😆 对话遇到了一些问题,不用慌:
|
||||
\ 1️⃣ 想要零配置开箱即用,[点击这里立刻开启对话 🚀](${SAAS_CHAT_URL_WITH_PARAM})
|
||||
\ 1️⃣ 想要零配置开箱即用,[点击这里立刻开启对话 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ 如果你正在使用私有部署版本,点击[这里](/#/auth)输入访问秘钥 🔑
|
||||
\ 3️⃣ 如果你想消耗自己的 OpenAI 资源,点击[这里](/#/settings)修改设置 ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const cs: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const cs: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 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_URL})
|
||||
\\ 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_URL})
|
||||
\ 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í ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const de: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const de: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 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_URL})
|
||||
\\ 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_URL})
|
||||
\ 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 ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { LocaleType } from "./index";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
// if you are adding a new translation, please use PartialLocaleType instead of LocaleType
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
@@ -10,10 +10,10 @@ const en: LocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 Oops, there's an issue. No worries:
|
||||
\\ 1️⃣ New here? [Click to start chatting now 🚀](${SAAS_CHAT_URL})
|
||||
\\ 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_URL})
|
||||
\ 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 ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const es: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const es: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 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_URL})
|
||||
\\ 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_URL})
|
||||
\ 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 ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const fr: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const fr: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 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_URL})
|
||||
\\ 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_URL})
|
||||
\ 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 ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const id: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const id: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 Percakapan mengalami beberapa masalah, tidak perlu khawatir:
|
||||
\\ 1️⃣ Jika Anda ingin memulai tanpa konfigurasi, [klik di sini untuk mulai mengobrol segera 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ Jika Anda ingin memulai tanpa konfigurasi, [klik di sini untuk mulai mengobrol segera 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Jika Anda ingin menggunakan sumber daya OpenAI Anda sendiri, klik [di sini](/#/settings) untuk mengubah pengaturan ⚙️`
|
||||
: `😆 Percakapan mengalami beberapa masalah, tidak perlu khawatir:
|
||||
\ 1️⃣ Jika Anda ingin memulai tanpa konfigurasi, [klik di sini untuk mulai mengobrol segera 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ Jika Anda ingin memulai tanpa konfigurasi, [klik di sini untuk mulai mengobrol segera 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Jika Anda menggunakan versi penyebaran pribadi, klik [di sini](/#/auth) untuk memasukkan kunci akses 🔑
|
||||
\ 3️⃣ Jika Anda ingin menggunakan sumber daya OpenAI Anda sendiri, klik [di sini](/#/settings) untuk mengubah pengaturan ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const it: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const it: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 La conversazione ha incontrato alcuni problemi, non preoccuparti:
|
||||
\\ 1️⃣ Se vuoi iniziare senza configurazione, [clicca qui per iniziare a chattare immediatamente 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ Se vuoi iniziare senza configurazione, [clicca qui per iniziare a chattare immediatamente 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Se vuoi utilizzare le tue risorse OpenAI, clicca [qui](/#/settings) per modificare le impostazioni ⚙️`
|
||||
: `😆 La conversazione ha incontrato alcuni problemi, non preoccuparti:
|
||||
\ 1️⃣ Se vuoi iniziare senza configurazione, [clicca qui per iniziare a chattare immediatamente 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ Se vuoi iniziare senza configurazione, [clicca qui per iniziare a chattare immediatamente 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Se stai utilizzando una versione di distribuzione privata, clicca [qui](/#/auth) per inserire la chiave di accesso 🔑
|
||||
\ 3️⃣ Se vuoi utilizzare le tue risorse OpenAI, clicca [qui](/#/settings) per modificare le impostazioni ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const jp: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const jp: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 会話中に問題が発生しましたが、心配しないでください:
|
||||
\\ 1️⃣ 設定なしで始めたい場合は、[ここをクリックしてすぐにチャットを開始 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ 設定なしで始めたい場合は、[ここをクリックしてすぐにチャットを開始 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ 自分のOpenAIリソースを使用したい場合は、[ここをクリックして](/#/settings)設定を変更してください ⚙️`
|
||||
: `😆 会話中に問題が発生しましたが、心配しないでください:
|
||||
\ 1️⃣ 設定なしで始めたい場合は、[ここをクリックしてすぐにチャットを開始 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ 設定なしで始めたい場合は、[ここをクリックしてすぐにチャットを開始 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ プライベートデプロイ版を使用している場合は、[ここをクリックして](/#/auth)アクセストークンを入力してください 🔑
|
||||
\ 3️⃣ 自分のOpenAIリソースを使用したい場合は、[ここをクリックして](/#/settings)設定を変更してください ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const ko: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const ko: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 대화 중 문제가 발생했습니다, 걱정하지 마세요:
|
||||
\\ 1️⃣ 제로 구성으로 시작하고 싶다면, [여기를 클릭하여 즉시 대화를 시작하세요 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ 제로 구성으로 시작하고 싶다면, [여기를 클릭하여 즉시 대화를 시작하세요 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ 자신의 OpenAI 리소스를 사용하고 싶다면, [여기를 클릭하여](/#/settings) 설정을 수정하세요 ⚙️`
|
||||
: `😆 대화 중 문제가 발생했습니다, 걱정하지 마세요:
|
||||
\ 1️⃣ 제로 구성으로 시작하고 싶다면, [여기를 클릭하여 즉시 대화를 시작하세요 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ 제로 구성으로 시작하고 싶다면, [여기를 클릭하여 즉시 대화를 시작하세요 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ 개인 배포 버전을 사용하고 있다면, [여기를 클릭하여](/#/auth) 접근 키를 입력하세요 🔑
|
||||
\ 3️⃣ 자신의 OpenAI 리소스를 사용하고 싶다면, [여기를 클릭하여](/#/settings) 설정을 수정하세요 ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const no: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const no: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 Samtalen har støtt på noen problemer, ikke bekymre deg:
|
||||
\\ 1️⃣ Hvis du vil starte uten konfigurasjon, [klikk her for å begynne å chatte umiddelbart 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ Hvis du vil starte uten konfigurasjon, [klikk her for å begynne å chatte umiddelbart 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Hvis du vil bruke dine egne OpenAI-ressurser, klikk [her](/#/settings) for å endre innstillingene ⚙️`
|
||||
: `😆 Samtalen har støtt på noen problemer, ikke bekymre deg:
|
||||
\ 1️⃣ Hvis du vil starte uten konfigurasjon, [klikk her for å begynne å chatte umiddelbart 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ Hvis du vil starte uten konfigurasjon, [klikk her for å begynne å chatte umiddelbart 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Hvis du bruker en privat distribusjonsversjon, klikk [her](/#/auth) for å skrive inn tilgangsnøkkelen 🔑
|
||||
\ 3️⃣ Hvis du vil bruke dine egne OpenAI-ressurser, klikk [her](/#/settings) for å endre innstillingene ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { PartialLocaleType } from "../locales/index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const pt: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const pt: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 A conversa encontrou alguns problemas, não se preocupe:
|
||||
\\ 1️⃣ Se você quiser começar sem configuração, [clique aqui para começar a conversar imediatamente 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ Se você quiser começar sem configuração, [clique aqui para começar a conversar imediatamente 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Se você deseja usar seus próprios recursos OpenAI, clique [aqui](/#/settings) para modificar as configurações ⚙️`
|
||||
: `😆 A conversa encontrou alguns problemas, não se preocupe:
|
||||
\ 1️⃣ Se você quiser começar sem configuração, [clique aqui para começar a conversar imediatamente 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ Se você quiser começar sem configuração, [clique aqui para começar a conversar imediatamente 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Se você estiver usando uma versão de implantação privada, clique [aqui](/#/auth) para inserir a chave de acesso 🔑
|
||||
\ 3️⃣ Se você deseja usar seus próprios recursos OpenAI, clique [aqui](/#/settings) para modificar as configurações ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { PartialLocaleType } from "../locales/index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const ru: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const ru: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 В разговоре возникли некоторые проблемы, не переживайте:
|
||||
\\ 1️⃣ Если вы хотите начать без настройки, [нажмите здесь, чтобы немедленно начать разговор 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ Если вы хотите начать без настройки, [нажмите здесь, чтобы немедленно начать разговор 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Если вы хотите использовать свои ресурсы OpenAI, нажмите [здесь](/#/settings), чтобы изменить настройки ⚙️`
|
||||
: `😆 В разговоре возникли некоторые проблемы, не переживайте:
|
||||
\ 1️⃣ Если вы хотите начать без настройки, [нажмите здесь, чтобы немедленно начать разговор 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ Если вы хотите начать без настройки, [нажмите здесь, чтобы немедленно начать разговор 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Если вы используете частную версию развертывания, нажмите [здесь](/#/auth), чтобы ввести ключ доступа 🔑
|
||||
\ 3️⃣ Если вы хотите использовать свои ресурсы OpenAI, нажмите [здесь](/#/settings), чтобы изменить настройки ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
// if you are adding a new translation, please use PartialLocaleType instead of LocaleType
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
@@ -10,10 +10,10 @@ const sk: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 Rozhovor narazil na nejaké problémy, nebojte sa:
|
||||
\\ 1️⃣ Ak chcete začať bez konfigurácie, [kliknite sem, aby ste okamžite začali chatovať 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ Ak chcete začať bez konfigurácie, [kliknite sem, aby ste okamžite začali chatovať 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Ak chcete používať svoje vlastné zdroje OpenAI, kliknite [sem](/#/settings), aby ste upravili nastavenia ⚙️`
|
||||
: `😆 Rozhovor narazil na nejaké problémy, nebojte sa:
|
||||
\ 1️⃣ Ak chcete začať bez konfigurácie, [kliknite sem, aby ste okamžite začali chatovať 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ Ak chcete začať bez konfigurácie, [kliknite sem, aby ste okamžite začali chatovať 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Ak používate verziu súkromného nasadenia, kliknite [sem](/#/auth), aby ste zadali prístupový kľúč 🔑
|
||||
\ 3️⃣ Ak chcete používať svoje vlastné zdroje OpenAI, kliknite [sem](/#/settings), aby ste upravili nastavenia ⚙️
|
||||
`,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const tr: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const tr: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 Sohbet bazı sorunlarla karşılaştı, endişelenmeyin:
|
||||
\\ 1️⃣ Eğer sıfır yapılandırma ile başlamak istiyorsanız, [buraya tıklayarak hemen sohbete başlayın 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ Eğer sıfır yapılandırma ile başlamak istiyorsanız, [buraya tıklayarak hemen sohbete başlayın 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Kendi OpenAI kaynaklarınızı kullanmak istiyorsanız, [buraya tıklayarak](/#/settings) ayarları değiştirin ⚙️`
|
||||
: `😆 Sohbet bazı sorunlarla karşılaştı, endişelenmeyin:
|
||||
\ 1️⃣ Eğer sıfır yapılandırma ile başlamak istiyorsanız, [buraya tıklayarak hemen sohbete başlayın 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ Eğer sıfır yapılandırma ile başlamak istiyorsanız, [buraya tıklayarak hemen sohbete başlayın 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Eğer özel dağıtım sürümü kullanıyorsanız, [buraya tıklayarak](/#/auth) erişim anahtarını girin 🔑
|
||||
\ 3️⃣ Kendi OpenAI kaynaklarınızı kullanmak istiyorsanız, [buraya tıklayarak](/#/settings) ayarları değiştirin ⚙️
|
||||
`,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const tw = {
|
||||
@@ -8,12 +8,12 @@ const tw = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 對話遇到了一些問題,不用慌:
|
||||
\\ 1️⃣ 想要零配置開箱即用,[點擊這裡立刻開啟對話 🚀](${SAAS_CHAT_URL})
|
||||
\\ 2️⃣ 如果你想消耗自己的 OpenAI 資源,點擊[這裡](/#/settings)修改設定 ⚙️`
|
||||
\\ 1️⃣ 想要無須設定開箱即用,[點選這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ 如果你想消耗自己的 OpenAI 資源,點選[這裡](/#/settings)修改設定 ⚙️`
|
||||
: `😆 對話遇到了一些問題,不用慌:
|
||||
\ 1️⃣ 想要零配置開箱即用,[點擊這裡立刻開啟對話 🚀](${SAAS_CHAT_URL})
|
||||
\ 2️⃣ 如果你正在使用私有部署版本,點擊[這裡](/#/auth)輸入訪問秘鑰 🔑
|
||||
\ 3️⃣ 如果你想消耗自己的 OpenAI 資源,點擊[這裡](/#/settings)修改設定 ⚙️
|
||||
\ 1️⃣ 想要無須設定開箱即用,[點選這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ 如果你正在使用私有部署版本,點選[這裡](/#/auth)輸入存取金鑰 🔑
|
||||
\ 3️⃣ 如果你想消耗自己的 OpenAI 資源,點選[這裡](/#/settings)修改設定 ⚙️
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -25,9 +25,9 @@ const tw = {
|
||||
Confirm: "確認",
|
||||
Later: "稍候再說",
|
||||
Return: "返回",
|
||||
SaasTips: "配置太麻煩,想要立即使用",
|
||||
SaasTips: "設定太麻煩,想要立即使用",
|
||||
TopTips:
|
||||
"🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
|
||||
"🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新的大型語言模型",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} 則對話`,
|
||||
@@ -53,8 +53,8 @@ const tw = {
|
||||
PinToastAction: "檢視",
|
||||
Delete: "刪除",
|
||||
Edit: "編輯",
|
||||
RefreshTitle: "刷新標題",
|
||||
RefreshToast: "已發送刷新標題請求",
|
||||
RefreshTitle: "重新整理標題",
|
||||
RefreshToast: "已傳送重新整理標題請求",
|
||||
},
|
||||
Commands: {
|
||||
new: "新建聊天",
|
||||
@@ -95,10 +95,10 @@ const tw = {
|
||||
IsContext: "預設提示詞",
|
||||
ShortcutKey: {
|
||||
Title: "鍵盤快捷方式",
|
||||
newChat: "打開新聊天",
|
||||
newChat: "開啟新聊天",
|
||||
focusInput: "聚焦輸入框",
|
||||
copyLastMessage: "複製最後一個回覆",
|
||||
copyLastCode: "複製最後一個代碼塊",
|
||||
copyLastCode: "複製最後一個程式碼區塊",
|
||||
showShortcutKey: "顯示快捷方式",
|
||||
},
|
||||
},
|
||||
@@ -174,9 +174,9 @@ const tw = {
|
||||
SubTitle: "聊天內容的字型大小",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "聊天字體",
|
||||
SubTitle: "聊天內容的字體,若置空則應用全局默認字體",
|
||||
Placeholder: "字體名稱",
|
||||
Title: "聊天字型",
|
||||
SubTitle: "聊天內容的字型,若留空則套用全域預設字型",
|
||||
Placeholder: "字型名稱",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "匯入系統提示",
|
||||
@@ -301,8 +301,8 @@ const tw = {
|
||||
Title: "使用 NextChat AI",
|
||||
Label: "(性價比最高的方案)",
|
||||
SubTitle:
|
||||
"由 NextChat 官方維護,零配置開箱即用,支持 OpenAI o1、GPT-4o、Claude-3.5 等最新大模型",
|
||||
ChatNow: "立刻對話",
|
||||
"由 NextChat 官方維護,無須設定開箱即用,支援 OpenAI o1、GPT-4o、Claude-3.5 等最新的大型語言模型",
|
||||
ChatNow: "立刻開始對話",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
@@ -485,18 +485,18 @@ const tw = {
|
||||
},
|
||||
},
|
||||
SearchChat: {
|
||||
Name: "搜索",
|
||||
Name: "搜尋",
|
||||
Page: {
|
||||
Title: "搜索聊天記錄",
|
||||
Search: "輸入搜索關鍵詞",
|
||||
Title: "搜尋聊天記錄",
|
||||
Search: "輸入搜尋關鍵詞",
|
||||
NoResult: "沒有找到結果",
|
||||
NoData: "沒有數據",
|
||||
Loading: "加載中",
|
||||
NoData: "沒有資料",
|
||||
Loading: "載入中",
|
||||
|
||||
SubTitle: (count: number) => `找到 ${count} 條結果`,
|
||||
},
|
||||
Item: {
|
||||
View: "查看",
|
||||
View: "檢視",
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SAAS_CHAT_URL } from "@/app/constant";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const vi: PartialLocaleType = {
|
||||
@@ -9,10 +9,10 @@ const vi: PartialLocaleType = {
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? `😆 Cuộc trò chuyện gặp một số vấn đề, đừng lo lắng:
|
||||
\\ 1️⃣ Nếu bạn muốn bắt đầu mà không cần cấu hình, [nhấp vào đây để bắt đầu trò chuyện ngay lập tức 🚀](${SAAS_CHAT_URL})
|
||||
\\ 1️⃣ Nếu bạn muốn bắt đầu mà không cần cấu hình, [nhấp vào đây để bắt đầu trò chuyện ngay lập tức 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Nếu bạn muốn sử dụng tài nguyên OpenAI của riêng mình, hãy nhấp [vào đây](/#/settings) để thay đổi cài đặt ⚙️`
|
||||
: `😆 Cuộc trò chuyện gặp một số vấn đề, đừng lo lắng:
|
||||
\ 1️⃣ Nếu bạn muốn bắt đầu mà không cần cấu hình, [nhấp vào đây để bắt đầu trò chuyện ngay lập tức 🚀](${SAAS_CHAT_URL})
|
||||
\ 1️⃣ Nếu bạn muốn bắt đầu mà không cần cấu hình, [nhấp vào đây để bắt đầu trò chuyện ngay lập tức 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Nếu bạn đang sử dụng phiên bản triển khai riêng, hãy nhấp [vào đây](/#/auth) để nhập khóa truy cập 🔑
|
||||
\ 3️⃣ Nếu bạn muốn sử dụng tài nguyên OpenAI của riêng mình, hãy nhấp [vào đây](/#/settings) để thay đổi cài đặt ⚙️
|
||||
`,
|
||||
|
@@ -1,9 +1,18 @@
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
GoogleSafetySettingsThreshold,
|
||||
ServiceProvider,
|
||||
StoreKey,
|
||||
ApiPath,
|
||||
OPENAI_BASE_URL,
|
||||
ANTHROPIC_BASE_URL,
|
||||
GEMINI_BASE_URL,
|
||||
BAIDU_BASE_URL,
|
||||
BYTEDANCE_BASE_URL,
|
||||
ALIBABA_BASE_URL,
|
||||
TENCENT_BASE_URL,
|
||||
MOONSHOT_BASE_URL,
|
||||
STABILITY_BASE_URL,
|
||||
IFLYTEK_BASE_URL,
|
||||
} from "../constant";
|
||||
import { getHeaders } from "../client/api";
|
||||
import { getClientConfig } from "../config/client";
|
||||
@@ -15,45 +24,25 @@ let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||
|
||||
const isApp = getClientConfig()?.buildMode === "export";
|
||||
|
||||
const DEFAULT_OPENAI_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/openai"
|
||||
: ApiPath.OpenAI;
|
||||
const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI;
|
||||
|
||||
const DEFAULT_GOOGLE_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/google"
|
||||
: ApiPath.Google;
|
||||
const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
||||
|
||||
const DEFAULT_ANTHROPIC_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/anthropic"
|
||||
: ApiPath.Anthropic;
|
||||
const DEFAULT_ANTHROPIC_URL = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
|
||||
|
||||
const DEFAULT_BAIDU_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/baidu"
|
||||
: ApiPath.Baidu;
|
||||
const DEFAULT_BAIDU_URL = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
|
||||
|
||||
const DEFAULT_BYTEDANCE_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/bytedance"
|
||||
: ApiPath.ByteDance;
|
||||
const DEFAULT_BYTEDANCE_URL = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
|
||||
|
||||
const DEFAULT_ALIBABA_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/alibaba"
|
||||
: ApiPath.Alibaba;
|
||||
const DEFAULT_ALIBABA_URL = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba;
|
||||
|
||||
const DEFAULT_TENCENT_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||
: ApiPath.Tencent;
|
||||
const DEFAULT_TENCENT_URL = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
|
||||
|
||||
const DEFAULT_MOONSHOT_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/moonshot"
|
||||
: ApiPath.Moonshot;
|
||||
const DEFAULT_MOONSHOT_URL = isApp ? MOONSHOT_BASE_URL : ApiPath.Moonshot;
|
||||
|
||||
const DEFAULT_STABILITY_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/stability"
|
||||
: ApiPath.Stability;
|
||||
const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability;
|
||||
|
||||
const DEFAULT_IFLYTEK_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/iflytek"
|
||||
: ApiPath.Iflytek;
|
||||
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
|
||||
|
||||
const DEFAULT_ACCESS_STATE = {
|
||||
accessCode: "",
|
||||
@@ -211,10 +200,13 @@ export const useAccessStore = createPersistStore(
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
// Set default model from env request
|
||||
let defaultModel = res.defaultModel ?? "";
|
||||
if (defaultModel !== "")
|
||||
DEFAULT_CONFIG.modelConfig.model = defaultModel;
|
||||
const defaultModel = res.defaultModel ?? "";
|
||||
if (defaultModel !== "") {
|
||||
const [model, providerName] = defaultModel.split("@");
|
||||
DEFAULT_CONFIG.modelConfig.model = model;
|
||||
DEFAULT_CONFIG.modelConfig.providerName = providerName;
|
||||
}
|
||||
|
||||
return res;
|
||||
})
|
||||
.then((res: DangerConfig) => {
|
||||
|
@@ -16,6 +16,9 @@ import {
|
||||
DEFAULT_SYSTEM_TEMPLATE,
|
||||
KnowledgeCutOffDate,
|
||||
StoreKey,
|
||||
SUMMARIZE_MODEL,
|
||||
GEMINI_SUMMARIZE_MODEL,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import Locale, { getLang } from "../locales";
|
||||
import { isDalle3, safeLocalStorage } from "../utils";
|
||||
@@ -23,6 +26,8 @@ import { prettyObject } from "../utils/format";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { estimateTokenLength } from "../utils/token";
|
||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { useAccessStore } from "./access";
|
||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
@@ -37,6 +42,7 @@ export type ChatMessageTool = {
|
||||
};
|
||||
content?: string;
|
||||
isError?: boolean;
|
||||
errorMsg?: string;
|
||||
};
|
||||
|
||||
export type ChatMessage = RequestMessage & {
|
||||
@@ -102,6 +108,35 @@ function createEmptySession(): ChatSession {
|
||||
};
|
||||
}
|
||||
|
||||
function getSummarizeModel(
|
||||
currentModel: string,
|
||||
providerName: string,
|
||||
): string[] {
|
||||
// if it is using gpt-* models, force to use 4o-mini to summarize
|
||||
if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) {
|
||||
const configStore = useAppConfig.getState();
|
||||
const accessStore = useAccessStore.getState();
|
||||
const allModel = collectModelsWithDefaultModel(
|
||||
configStore.models,
|
||||
[configStore.customModels, accessStore.customModels].join(","),
|
||||
accessStore.defaultModel,
|
||||
);
|
||||
const summarizeModel = allModel.find(
|
||||
(m) => m.name === SUMMARIZE_MODEL && m.available,
|
||||
);
|
||||
if (summarizeModel) {
|
||||
return [
|
||||
summarizeModel.name,
|
||||
summarizeModel.provider?.providerName as string,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (currentModel.startsWith("gemini")) {
|
||||
return [GEMINI_SUMMARIZE_MODEL, ServiceProvider.Google];
|
||||
}
|
||||
return [currentModel, providerName];
|
||||
}
|
||||
|
||||
function countMessages(msgs: ChatMessage[]) {
|
||||
return msgs.reduce(
|
||||
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
|
||||
@@ -578,8 +613,14 @@ export const useChatStore = createPersistStore(
|
||||
return;
|
||||
}
|
||||
|
||||
const providerName = modelConfig.compressProviderName;
|
||||
const api: ClientApi = getClientApi(providerName);
|
||||
// if not config compressModel, then using getSummarizeModel
|
||||
const [model, providerName] = modelConfig.compressModel
|
||||
? [modelConfig.compressModel, modelConfig.compressProviderName]
|
||||
: getSummarizeModel(
|
||||
session.mask.modelConfig.model,
|
||||
session.mask.modelConfig.providerName,
|
||||
);
|
||||
const api: ClientApi = getClientApi(providerName as ServiceProvider);
|
||||
|
||||
// remove error messages if any
|
||||
const messages = session.messages;
|
||||
@@ -610,11 +651,12 @@ export const useChatStore = createPersistStore(
|
||||
api.llm.chat({
|
||||
messages: topicMessages,
|
||||
config: {
|
||||
model: modelConfig.compressModel,
|
||||
model,
|
||||
stream: false,
|
||||
providerName,
|
||||
},
|
||||
onFinish(message) {
|
||||
if (!isValidMessage(message)) return;
|
||||
get().updateCurrentSession(
|
||||
(session) =>
|
||||
(session.topic =
|
||||
@@ -673,7 +715,8 @@ export const useChatStore = createPersistStore(
|
||||
config: {
|
||||
...modelcfg,
|
||||
stream: true,
|
||||
model: modelConfig.compressModel,
|
||||
model,
|
||||
providerName,
|
||||
},
|
||||
onUpdate(message) {
|
||||
session.memoryPrompt = message;
|
||||
@@ -690,6 +733,10 @@ export const useChatStore = createPersistStore(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function isValidMessage(message: any): boolean {
|
||||
return typeof message === "string" && !message.startsWith("```json");
|
||||
}
|
||||
},
|
||||
|
||||
updateStat(message: ChatMessage) {
|
||||
@@ -722,7 +769,7 @@ export const useChatStore = createPersistStore(
|
||||
},
|
||||
{
|
||||
name: StoreKey.Chat,
|
||||
version: 3.2,
|
||||
version: 3.3,
|
||||
migrate(persistedState, version) {
|
||||
const state = persistedState as any;
|
||||
const newState = JSON.parse(
|
||||
@@ -778,6 +825,14 @@ export const useChatStore = createPersistStore(
|
||||
config.modelConfig.compressProviderName;
|
||||
});
|
||||
}
|
||||
// revert default summarize model for every session
|
||||
if (version < 3.3) {
|
||||
newState.sessions.forEach((s) => {
|
||||
const config = useAppConfig.getState();
|
||||
s.mask.modelConfig.compressModel = "";
|
||||
s.mask.modelConfig.compressProviderName = "";
|
||||
});
|
||||
}
|
||||
|
||||
return newState as any;
|
||||
},
|
||||
|
@@ -50,6 +50,8 @@ export const DEFAULT_CONFIG = {
|
||||
enableAutoGenerateTitle: true,
|
||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||
|
||||
enableArtifacts: true, // show artifacts config
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
|
||||
@@ -63,14 +65,14 @@ export const DEFAULT_CONFIG = {
|
||||
providerName: "OpenAI" as ServiceProvider,
|
||||
temperature: 0.5,
|
||||
top_p: 1,
|
||||
max_tokens: 4000,
|
||||
max_completions_tokens: 4000,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
sendMemory: true,
|
||||
historyMessageCount: 4,
|
||||
compressMessageLengthThreshold: 1000,
|
||||
compressModel: "gpt-4o-mini" as ModelType,
|
||||
compressProviderName: "OpenAI" as ServiceProvider,
|
||||
compressModel: "",
|
||||
compressProviderName: "",
|
||||
enableInjectSystemPrompts: true,
|
||||
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
||||
size: "1024x1024" as DalleSize,
|
||||
@@ -125,7 +127,7 @@ export const ModalConfigValidator = {
|
||||
model(x: string) {
|
||||
return x as ModelType;
|
||||
},
|
||||
max_tokens(x: number) {
|
||||
max_completions_tokens(x: number) {
|
||||
return limitNumber(x, 0, 512000, 1024);
|
||||
},
|
||||
presence_penalty(x: number) {
|
||||
@@ -176,7 +178,7 @@ export const useAppConfig = createPersistStore(
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Config,
|
||||
version: 4,
|
||||
version: 4.1,
|
||||
|
||||
merge(persistedState, currentState) {
|
||||
const state = persistedState as ChatConfig | undefined;
|
||||
@@ -229,7 +231,7 @@ export const useAppConfig = createPersistStore(
|
||||
: config?.template ?? DEFAULT_INPUT_TEMPLATE;
|
||||
}
|
||||
|
||||
if (version < 4) {
|
||||
if (version < 4.1) {
|
||||
state.modelConfig.compressModel =
|
||||
DEFAULT_CONFIG.modelConfig.compressModel;
|
||||
state.modelConfig.compressProviderName =
|
||||
|
@@ -2,8 +2,12 @@ import OpenAPIClientAxios from "openapi-client-axios";
|
||||
import { StoreKey } from "../constant";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import yaml from "js-yaml";
|
||||
import { adapter } from "../utils";
|
||||
import { adapter, getOperationId } from "../utils";
|
||||
import { useAccessStore } from "./access";
|
||||
|
||||
const isApp = getClientConfig()?.isApp !== false;
|
||||
|
||||
export type Plugin = {
|
||||
id: string;
|
||||
@@ -16,7 +20,6 @@ export type Plugin = {
|
||||
authLocation?: string;
|
||||
authHeader?: string;
|
||||
authToken?: string;
|
||||
usingProxy?: boolean;
|
||||
};
|
||||
|
||||
export type FunctionToolItem = {
|
||||
@@ -46,18 +49,25 @@ export const FunctionToolService = {
|
||||
plugin?.authType == "basic"
|
||||
? `Basic ${plugin?.authToken}`
|
||||
: plugin?.authType == "bearer"
|
||||
? ` Bearer ${plugin?.authToken}`
|
||||
? `Bearer ${plugin?.authToken}`
|
||||
: plugin?.authToken;
|
||||
const authLocation = plugin?.authLocation || "header";
|
||||
const definition = yaml.load(plugin.content) as any;
|
||||
const serverURL = definition?.servers?.[0]?.url;
|
||||
const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL;
|
||||
const baseURL = !isApp ? "/api/proxy" : serverURL;
|
||||
const headers: Record<string, string | undefined> = {
|
||||
"X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
|
||||
"X-Base-URL": !isApp ? serverURL : undefined,
|
||||
};
|
||||
if (authLocation == "header") {
|
||||
headers[headerName] = tokenValue;
|
||||
}
|
||||
// try using openaiApiKey for Dalle3 Plugin.
|
||||
if (!tokenValue && plugin.id === "dalle3") {
|
||||
const openaiApiKey = useAccessStore.getState().openaiApiKey;
|
||||
if (openaiApiKey) {
|
||||
headers[headerName] = `Bearer ${openaiApiKey}`;
|
||||
}
|
||||
}
|
||||
const api = new OpenAPIClientAxios({
|
||||
definition: yaml.load(plugin.content) as any,
|
||||
axiosConfigDefaults: {
|
||||
@@ -106,7 +116,7 @@ export const FunctionToolService = {
|
||||
return {
|
||||
type: "function",
|
||||
function: {
|
||||
name: o.operationId,
|
||||
name: getOperationId(o),
|
||||
description: o.description || o.summary,
|
||||
parameters: parameters,
|
||||
},
|
||||
@@ -114,7 +124,7 @@ export const FunctionToolService = {
|
||||
}),
|
||||
funcs: operations.reduce((s, o) => {
|
||||
// @ts-ignore
|
||||
s[o.operationId] = function (args) {
|
||||
s[getOperationId(o)] = function (args) {
|
||||
const parameters: Record<string, any> = {};
|
||||
if (o.parameters instanceof Array) {
|
||||
o.parameters.forEach((p) => {
|
||||
@@ -129,8 +139,8 @@ export const FunctionToolService = {
|
||||
} else if (authLocation == "body") {
|
||||
args[headerName] = tokenValue;
|
||||
}
|
||||
// @ts-ignore
|
||||
return api.client[o.operationId](
|
||||
// @ts-ignore if o.operationId is null, then using o.path and o.method
|
||||
return api.client.paths[o.path][o.method](
|
||||
parameters,
|
||||
args,
|
||||
api.axiosConfigDefaults,
|
||||
@@ -165,7 +175,7 @@ export const usePluginStore = createPersistStore(
|
||||
(set, get) => ({
|
||||
create(plugin?: Partial<Plugin>) {
|
||||
const plugins = get().plugins;
|
||||
const id = nanoid();
|
||||
const id = plugin?.id || nanoid();
|
||||
plugins[id] = {
|
||||
...createEmptyPlugin(),
|
||||
...plugin,
|
||||
@@ -220,5 +230,42 @@ export const usePluginStore = createPersistStore(
|
||||
{
|
||||
name: StoreKey.Plugin,
|
||||
version: 1,
|
||||
onRehydrateStorage(state) {
|
||||
// Skip store rehydration on server side
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("./plugins.json")
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
Promise.all(
|
||||
res.map((item: any) =>
|
||||
// skip get schema
|
||||
state.get(item.id)
|
||||
? item
|
||||
: fetch(item.schema)
|
||||
.then((res) => res.text())
|
||||
.then((content) => ({
|
||||
...item,
|
||||
content,
|
||||
}))
|
||||
.catch((e) => item),
|
||||
),
|
||||
).then((builtinPlugins: any) => {
|
||||
builtinPlugins
|
||||
.filter((item: any) => item?.content)
|
||||
.forEach((item: any) => {
|
||||
const plugin = state.create(item);
|
||||
state.updatePlugin(plugin.id, (plugin) => {
|
||||
const tool = FunctionToolService.add(plugin, true);
|
||||
plugin.title = tool.api.definition.info.title;
|
||||
plugin.version = tool.api.definition.info.version;
|
||||
plugin.builtin = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { getLang } from "../locales";
|
||||
import { StoreKey } from "../constant";
|
||||
import { nanoid } from "nanoid";
|
||||
import { StoreKey } from "../constant";
|
||||
import { getLang } from "../locales";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
|
||||
export interface Prompt {
|
||||
@@ -147,6 +147,11 @@ export const usePromptStore = createPersistStore(
|
||||
},
|
||||
|
||||
onRehydrateStorage(state) {
|
||||
// Skip store rehydration on server side
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const PROMPT_URL = "./prompts.json";
|
||||
|
||||
type PromptList = Array<[string, string]>;
|
||||
|
@@ -12,7 +12,6 @@ import { downloadAs, readFromFile } from "../utils";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
import Locale from "../locales";
|
||||
import { createSyncClient, ProviderType } from "../utils/cloud";
|
||||
import { corsPath } from "../utils/cors";
|
||||
|
||||
export interface WebDavConfig {
|
||||
server: string;
|
||||
@@ -26,7 +25,7 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||
const DEFAULT_SYNC_STATE = {
|
||||
provider: ProviderType.WebDAV,
|
||||
useProxy: true,
|
||||
proxyUrl: corsPath(ApiPath.Cors),
|
||||
proxyUrl: ApiPath.Cors as string,
|
||||
|
||||
webdav: {
|
||||
endpoint: "",
|
||||
|
43
app/utils.ts
43
app/utils.ts
@@ -2,8 +2,9 @@ import { useEffect, useState } from "react";
|
||||
import { showToast } from "./components/ui-lib";
|
||||
import Locale from "./locales";
|
||||
import { RequestMessage } from "./client/api";
|
||||
import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
|
||||
import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
|
||||
import { ServiceProvider } from "./constant";
|
||||
// import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
|
||||
import { fetch as tauriStreamFetch } from "./utils/stream";
|
||||
|
||||
export function trimTopic(topic: string) {
|
||||
// Fix an issue where double quotes still show in the Indonesian language
|
||||
@@ -284,6 +285,9 @@ export function showPlugins(provider: ServiceProvider, model: string) {
|
||||
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
||||
return true;
|
||||
}
|
||||
if (provider == ServiceProvider.Google && !model.includes("vision")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -292,30 +296,23 @@ export function fetch(
|
||||
options?: Record<string, unknown>,
|
||||
): Promise<any> {
|
||||
if (window.__TAURI__) {
|
||||
const payload = options?.body || options?.data;
|
||||
return tauriFetch(url, {
|
||||
...options,
|
||||
body:
|
||||
payload &&
|
||||
({
|
||||
type: "Text",
|
||||
payload,
|
||||
} as any),
|
||||
timeout: ((options?.timeout as number) || REQUEST_TIMEOUT_MS) / 1000,
|
||||
responseType:
|
||||
options?.responseType == "text" ? ResponseType.Text : ResponseType.JSON,
|
||||
} as any);
|
||||
return tauriStreamFetch(url, options);
|
||||
}
|
||||
return window.fetch(url, options);
|
||||
}
|
||||
|
||||
export function adapter(config: Record<string, unknown>) {
|
||||
const { baseURL, url, params, ...rest } = config;
|
||||
const { baseURL, url, params, data: body, ...rest } = config;
|
||||
const path = baseURL ? `${baseURL}${url}` : url;
|
||||
const fetchUrl = params
|
||||
? `${path}?${new URLSearchParams(params as any).toString()}`
|
||||
: path;
|
||||
return fetch(fetchUrl as string, { ...rest, responseType: "text" });
|
||||
return fetch(fetchUrl as string, { ...rest, body }).then((res) => {
|
||||
const { status, headers, statusText } = res;
|
||||
return res
|
||||
.text()
|
||||
.then((data: string) => ({ status, statusText, headers, data }));
|
||||
});
|
||||
}
|
||||
|
||||
export function safeLocalStorage(): {
|
||||
@@ -377,3 +374,15 @@ export function safeLocalStorage(): {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getOperationId(operation: {
|
||||
operationId?: string;
|
||||
method: string;
|
||||
path: string;
|
||||
}) {
|
||||
// pattern '^[a-zA-Z0-9_-]+$'
|
||||
return (
|
||||
operation?.operationId ||
|
||||
`${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}`
|
||||
);
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "./format";
|
||||
import { fetch as tauriFetch } from "./stream";
|
||||
|
||||
export function compressImage(file: Blob, maxSize: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -221,7 +222,12 @@ export function stream(
|
||||
),
|
||||
)
|
||||
.then((res) => {
|
||||
const content = JSON.stringify(res.data);
|
||||
let content = res.data || res?.statusText;
|
||||
// hotfix #5614
|
||||
content =
|
||||
typeof content === "string"
|
||||
? content
|
||||
: JSON.stringify(content);
|
||||
if (res.status >= 300) {
|
||||
return Promise.reject(content);
|
||||
}
|
||||
@@ -236,10 +242,15 @@ export function stream(
|
||||
return content;
|
||||
})
|
||||
.catch((e) => {
|
||||
options?.onAfterTool?.({ ...tool, isError: true });
|
||||
options?.onAfterTool?.({
|
||||
...tool,
|
||||
isError: true,
|
||||
errorMsg: e.toString(),
|
||||
});
|
||||
return e.toString();
|
||||
})
|
||||
.then((content) => ({
|
||||
name: tool.function.name,
|
||||
role: "tool",
|
||||
content,
|
||||
tool_call_id: tool.id,
|
||||
@@ -287,6 +298,7 @@ export function stream(
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: tauriFetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
@@ -1,19 +0,0 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { DEFAULT_API_HOST } from "../constant";
|
||||
|
||||
export function corsPath(path: string) {
|
||||
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_API_HOST}` : "";
|
||||
|
||||
if (baseUrl === "" && path === "") {
|
||||
return "";
|
||||
}
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
|
||||
if (!path.endsWith("/")) {
|
||||
path += "/";
|
||||
}
|
||||
|
||||
return `${baseUrl}${path}`;
|
||||
}
|
107
app/utils/stream.ts
Normal file
107
app/utils/stream.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
// using tauri command to send request
|
||||
// see src-tauri/src/stream.rs, and src-tauri/src/main.rs
|
||||
// 1. invoke('stream_fetch', {url, method, headers, body}), get response with headers.
|
||||
// 2. listen event: `stream-response` multi times to get body
|
||||
|
||||
type ResponseEvent = {
|
||||
id: number;
|
||||
payload: {
|
||||
request_id: number;
|
||||
status?: number;
|
||||
chunk?: number[];
|
||||
};
|
||||
};
|
||||
|
||||
type StreamResponse = {
|
||||
request_id: number;
|
||||
status: number;
|
||||
status_text: string;
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
export function fetch(url: string, options?: RequestInit): Promise<any> {
|
||||
if (window.__TAURI__) {
|
||||
const {
|
||||
signal,
|
||||
method = "GET",
|
||||
headers: _headers = {},
|
||||
body = [],
|
||||
} = options || {};
|
||||
let unlisten: Function | undefined;
|
||||
let setRequestId: Function | undefined;
|
||||
const requestIdPromise = new Promise((resolve) => (setRequestId = resolve));
|
||||
const ts = new TransformStream();
|
||||
const writer = ts.writable.getWriter();
|
||||
|
||||
let closed = false;
|
||||
const close = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
unlisten && unlisten();
|
||||
writer.ready.then(() => {
|
||||
writer.close().catch((e) => console.error(e));
|
||||
});
|
||||
};
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", () => close());
|
||||
}
|
||||
// @ts-ignore 2. listen response multi times, and write to Response.body
|
||||
window.__TAURI__.event
|
||||
.listen("stream-response", (e: ResponseEvent) =>
|
||||
requestIdPromise.then((request_id) => {
|
||||
const { request_id: rid, chunk, status } = e?.payload || {};
|
||||
if (request_id != rid) {
|
||||
return;
|
||||
}
|
||||
if (chunk) {
|
||||
writer.ready.then(() => {
|
||||
writer.write(new Uint8Array(chunk));
|
||||
});
|
||||
} else if (status === 0) {
|
||||
// end of body
|
||||
close();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then((u: Function) => (unlisten = u));
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
"Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
|
||||
"User-Agent": navigator.userAgent,
|
||||
};
|
||||
for (const item of new Headers(_headers || {})) {
|
||||
headers[item[0]] = item[1];
|
||||
}
|
||||
return window.__TAURI__
|
||||
.invoke("stream_fetch", {
|
||||
method: method.toUpperCase(),
|
||||
url,
|
||||
headers,
|
||||
// TODO FormData
|
||||
body:
|
||||
typeof body === "string"
|
||||
? Array.from(new TextEncoder().encode(body))
|
||||
: [],
|
||||
})
|
||||
.then((res: StreamResponse) => {
|
||||
const { request_id, status, status_text: statusText, headers } = res;
|
||||
setRequestId?.(request_id);
|
||||
const response = new Response(ts.readable, {
|
||||
status,
|
||||
statusText,
|
||||
headers,
|
||||
});
|
||||
if (status >= 300) {
|
||||
setTimeout(close, 100);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("stream error", e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return window.fetch(url, options);
|
||||
}
|
21
jest.config.ts
Normal file
21
jest.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Config } from "jest";
|
||||
import nextJest from "next/jest.js";
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: "./",
|
||||
});
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
const config: Config = {
|
||||
coverageProvider: "v8",
|
||||
testEnvironment: "jsdom",
|
||||
testMatch: ["**/*.test.js", "**/*.test.ts", "**/*.test.jsx", "**/*.test.tsx"],
|
||||
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)$": "<rootDir>/$1",
|
||||
},
|
||||
};
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
export default createJestConfig(config);
|
2
jest.setup.ts
Normal file
2
jest.setup.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom";
|
16
package.json
16
package.json
@@ -6,16 +6,18 @@
|
||||
"mask": "npx tsx app/masks/build.ts",
|
||||
"mask:watch": "npx watch \"yarn mask\" app/masks",
|
||||
"dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"",
|
||||
"build": "yarn mask && cross-env BUILD_MODE=standalone next build",
|
||||
"build": "yarn test:ci && yarn mask && cross-env BUILD_MODE=standalone next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
||||
"export": "yarn test:ci && yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
||||
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
||||
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
||||
"app:build": "yarn mask && yarn tauri build",
|
||||
"app:build": "yarn test:ci && yarn mask && yarn tauri build",
|
||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||
"prepare": "husky install",
|
||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
|
||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
|
||||
"test": "jest --watch",
|
||||
"test:ci": "jest --ci"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortaine/fetch-event-source": "^3.0.6",
|
||||
@@ -54,6 +56,9 @@
|
||||
"devDependencies": {
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"@tauri-apps/cli": "1.5.11",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.30",
|
||||
@@ -69,8 +74,11 @@
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"husky": "^8.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"prettier": "^3.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.16.0",
|
||||
"typescript": "5.2.2",
|
||||
"watch": "^1.0.2",
|
||||
|
17
public/plugins.json
Normal file
17
public/plugins.json
Normal file
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"id": "dalle3",
|
||||
"name": "Dalle3",
|
||||
"schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/dalle/openapi.json"
|
||||
},
|
||||
{
|
||||
"id": "arxivsearch",
|
||||
"name": "ArxivSearch",
|
||||
"schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/arxivsearch/openapi.json"
|
||||
},
|
||||
{
|
||||
"id": "duckduckgolite",
|
||||
"name": "DuckDuckGoLiteSearch",
|
||||
"schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/duckduckgolite/openapi.json"
|
||||
}
|
||||
]
|
52
src-tauri/Cargo.lock
generated
52
src-tauri/Cargo.lock
generated
@@ -348,9 +348,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.4.0"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -942,9 +942,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.1.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
|
||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
@@ -970,9 +970,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
@@ -987,9 +987,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -1008,9 +1008,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1019,21 +1019,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
@@ -1555,9 +1555,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
@@ -1986,6 +1986,10 @@ checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||
name = "nextchat"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -2281,9 +2285,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.2.0"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
@@ -2545,9 +2549,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.58"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -3889,9 +3893,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.3.1"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
|
@@ -37,6 +37,10 @@ tauri = { version = "1.5.4", features = [ "http-all",
|
||||
"window-unminimize",
|
||||
] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
percent-encoding = "2.3.1"
|
||||
reqwest = "0.11.18"
|
||||
futures-util = "0.3.30"
|
||||
bytes = "1.7.2"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
|
@@ -1,8 +1,11 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod stream;
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![stream::stream_fetch])
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
134
src-tauri/src/stream.rs
Normal file
134
src-tauri/src/stream.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
//
|
||||
//
|
||||
|
||||
use std::time::Duration;
|
||||
use std::error::Error;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::collections::HashMap;
|
||||
use futures_util::{StreamExt};
|
||||
use reqwest::Client;
|
||||
use reqwest::header::{HeaderName, HeaderMap};
|
||||
|
||||
static REQUEST_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct StreamResponse {
|
||||
request_id: u32,
|
||||
status: u16,
|
||||
status_text: String,
|
||||
headers: HashMap<String, String>
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
pub struct EndPayload {
|
||||
request_id: u32,
|
||||
status: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
pub struct ChunkPayload {
|
||||
request_id: u32,
|
||||
chunk: bytes::Bytes,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stream_fetch(
|
||||
window: tauri::Window,
|
||||
method: String,
|
||||
url: String,
|
||||
headers: HashMap<String, String>,
|
||||
body: Vec<u8>,
|
||||
) -> Result<StreamResponse, String> {
|
||||
|
||||
let event_name = "stream-response";
|
||||
let request_id = REQUEST_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
let mut _headers = HeaderMap::new();
|
||||
for (key, value) in &headers {
|
||||
_headers.insert(key.parse::<HeaderName>().unwrap(), value.parse().unwrap());
|
||||
}
|
||||
|
||||
// println!("method: {:?}", method);
|
||||
// println!("url: {:?}", url);
|
||||
// println!("headers: {:?}", headers);
|
||||
// println!("headers: {:?}", _headers);
|
||||
|
||||
let method = method.parse::<reqwest::Method>().map_err(|err| format!("failed to parse method: {}", err))?;
|
||||
let client = Client::builder()
|
||||
.default_headers(_headers)
|
||||
.redirect(reqwest::redirect::Policy::limited(3))
|
||||
.connect_timeout(Duration::new(3, 0))
|
||||
.build()
|
||||
.map_err(|err| format!("failed to generate client: {}", err))?;
|
||||
|
||||
let mut request = client.request(
|
||||
method.clone(),
|
||||
url.parse::<reqwest::Url>().map_err(|err| format!("failed to parse url: {}", err))?
|
||||
);
|
||||
|
||||
if method == reqwest::Method::POST || method == reqwest::Method::PUT || method == reqwest::Method::PATCH {
|
||||
let body = bytes::Bytes::from(body);
|
||||
// println!("body: {:?}", body);
|
||||
request = request.body(body);
|
||||
}
|
||||
|
||||
// println!("client: {:?}", client);
|
||||
// println!("request: {:?}", request);
|
||||
|
||||
let response_future = request.send();
|
||||
|
||||
let res = response_future.await;
|
||||
let response = match res {
|
||||
Ok(res) => {
|
||||
// get response and emit to client
|
||||
let mut headers = HashMap::new();
|
||||
for (name, value) in res.headers() {
|
||||
headers.insert(
|
||||
name.as_str().to_string(),
|
||||
std::str::from_utf8(value.as_bytes()).unwrap().to_string()
|
||||
);
|
||||
}
|
||||
let status = res.status().as_u16();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut stream = res.bytes_stream();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
match chunk {
|
||||
Ok(bytes) => {
|
||||
// println!("chunk: {:?}", bytes);
|
||||
if let Err(e) = window.emit(event_name, ChunkPayload{ request_id, chunk: bytes }) {
|
||||
println!("Failed to emit chunk payload: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error chunk: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(e) = window.emit(event_name, EndPayload{ request_id, status: 0 }) {
|
||||
println!("Failed to emit end payload: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
StreamResponse {
|
||||
request_id,
|
||||
status,
|
||||
status_text: "OK".to_string(),
|
||||
headers,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error response: {:?}", err.source().expect("REASON").to_string());
|
||||
StreamResponse {
|
||||
request_id,
|
||||
status: 599,
|
||||
status_text: err.source().expect("REASON").to_string(),
|
||||
headers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
};
|
||||
// println!("Response: {:?}", response);
|
||||
Ok(response)
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "NextChat",
|
||||
"version": "2.15.2"
|
||||
"version": "2.15.4"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
9
test/sum-module.test.ts
Normal file
9
test/sum-module.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
function sum(a: number, b: number) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
describe("sum module", () => {
|
||||
test("adds 1 + 2 to equal 3", () => {
|
||||
expect(sum(1, 2)).toBe(3);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user