style: eslint 规则同步代码格式

This commit is contained in:
ocean-gao 2024-12-20 13:21:52 +08:00
parent 161fa63b02
commit 4b4fe29118
161 changed files with 12206 additions and 11827 deletions

View File

@ -1,80 +1,80 @@
name: '🐛 Bug Report'
description: 'Report an bug'
name: 🐛 Bug Report
description: Report an bug
title: '[Bug] '
labels: ['bug']
labels: [bug]
body:
- type: dropdown
attributes:
label: '📦 Deployment Method'
multiple: true
options:
- 'Official installation package'
- 'Vercel'
- 'Zeabur'
- 'Sealos'
- 'Netlify'
- 'Docker'
- 'Other'
validations:
required: true
- type: input
attributes:
label: '📌 Version'
validations:
required: true
- type: dropdown
attributes:
label: '💻 Operating System'
multiple: true
options:
- 'Windows'
- 'macOS'
- 'Ubuntu'
- 'Other Linux'
- 'iOS'
- 'iPad OS'
- 'Android'
- 'Other'
validations:
required: true
- type: input
attributes:
label: '📌 System Version'
validations:
required: true
- type: dropdown
attributes:
label: '🌐 Browser'
multiple: true
options:
- 'Chrome'
- 'Edge'
- 'Safari'
- 'Firefox'
- 'Other'
validations:
required: true
- type: input
attributes:
label: '📌 Browser Version'
validations:
required: true
- type: textarea
attributes:
label: '🐛 Bug Description'
description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
validations:
required: true
- type: textarea
attributes:
label: '📷 Recurrence Steps'
description: A clear and concise description of how to recurrence.
- type: textarea
attributes:
label: '🚦 Expected Behavior'
description: A clear and concise description of what you expected to happen.
- type: textarea
attributes:
label: '📝 Additional Information'
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
- type: dropdown
attributes:
label: 📦 Deployment Method
multiple: true
options:
- Official installation package
- Vercel
- Zeabur
- Sealos
- Netlify
- Docker
- Other
validations:
required: true
- type: input
attributes:
label: 📌 Version
validations:
required: true
- type: dropdown
attributes:
label: 💻 Operating System
multiple: true
options:
- Windows
- macOS
- Ubuntu
- Other Linux
- iOS
- iPad OS
- Android
- Other
validations:
required: true
- type: input
attributes:
label: 📌 System Version
validations:
required: true
- type: dropdown
attributes:
label: 🌐 Browser
multiple: true
options:
- Chrome
- Edge
- Safari
- Firefox
- Other
validations:
required: true
- type: input
attributes:
label: 📌 Browser Version
validations:
required: true
- type: textarea
attributes:
label: 🐛 Bug Description
description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
validations:
required: true
- type: textarea
attributes:
label: 📷 Recurrence Steps
description: A clear and concise description of how to recurrence.
- type: textarea
attributes:
label: 🚦 Expected Behavior
description: A clear and concise description of what you expected to happen.
- type: textarea
attributes:
label: 📝 Additional Information
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.

View File

@ -1,80 +1,80 @@
name: '🐛 反馈缺陷'
description: '反馈一个问题/缺陷'
name: 🐛 反馈缺陷
description: 反馈一个问题/缺陷
title: '[Bug] '
labels: ['bug']
labels: [bug]
body:
- type: dropdown
attributes:
label: '📦 部署方式'
multiple: true
options:
- '官方安装包'
- 'Vercel'
- 'Zeabur'
- 'Sealos'
- 'Netlify'
- 'Docker'
- 'Other'
validations:
required: true
- type: input
attributes:
label: '📌 软件版本'
validations:
required: true
- type: dropdown
attributes:
label: 📦 部署方式
multiple: true
options:
- 官方安装包
- Vercel
- Zeabur
- Sealos
- Netlify
- Docker
- Other
validations:
required: true
- type: input
attributes:
label: 📌 软件版本
validations:
required: true
- type: dropdown
attributes:
label: '💻 系统环境'
multiple: true
options:
- 'Windows'
- 'macOS'
- 'Ubuntu'
- 'Other Linux'
- 'iOS'
- 'iPad OS'
- 'Android'
- 'Other'
validations:
required: true
- type: input
attributes:
label: '📌 系统版本'
validations:
required: true
- type: dropdown
attributes:
label: '🌐 浏览器'
multiple: true
options:
- 'Chrome'
- 'Edge'
- 'Safari'
- 'Firefox'
- 'Other'
validations:
required: true
- type: input
attributes:
label: '📌 浏览器版本'
validations:
required: true
- type: textarea
attributes:
label: '🐛 问题描述'
description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。
validations:
required: true
- type: textarea
attributes:
label: '📷 复现步骤'
description: 请提供一个清晰且简洁的描述,说明如何复现问题。
- type: textarea
attributes:
label: '🚦 期望结果'
description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
- type: textarea
attributes:
label: '📝 补充信息'
description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。
- type: dropdown
attributes:
label: 💻 系统环境
multiple: true
options:
- Windows
- macOS
- Ubuntu
- Other Linux
- iOS
- iPad OS
- Android
- Other
validations:
required: true
- type: input
attributes:
label: 📌 系统版本
validations:
required: true
- type: dropdown
attributes:
label: 🌐 浏览器
multiple: true
options:
- Chrome
- Edge
- Safari
- Firefox
- Other
validations:
required: true
- type: input
attributes:
label: 📌 浏览器版本
validations:
required: true
- type: textarea
attributes:
label: 🐛 问题描述
description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。
validations:
required: true
- type: textarea
attributes:
label: 📷 复现步骤
description: 请提供一个清晰且简洁的描述,说明如何复现问题。
- type: textarea
attributes:
label: 🚦 期望结果
description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
- type: textarea
attributes:
label: 📝 补充信息
description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。

View File

@ -1,21 +1,21 @@
name: '🌠 Feature Request'
description: 'Suggest an idea'
name: 🌠 Feature Request
description: Suggest an idea
title: '[Feature Request] '
labels: ['enhancement']
labels: [enhancement]
body:
- type: textarea
attributes:
label: '🥰 Feature Description'
description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
validations:
required: true
- type: textarea
attributes:
label: '🧐 Proposed Solution'
description: Describe the solution you'd like in a clear and concise manner.
validations:
required: true
- type: textarea
attributes:
label: '📝 Additional Information'
description: Add any other context about the problem here.
- type: textarea
attributes:
label: 🥰 Feature Description
description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
validations:
required: true
- type: textarea
attributes:
label: 🧐 Proposed Solution
description: Describe the solution you'd like in a clear and concise manner.
validations:
required: true
- type: textarea
attributes:
label: 📝 Additional Information
description: Add any other context about the problem here.

View File

@ -1,21 +1,21 @@
name: '🌠 功能需求'
description: '提出需求或建议'
name: 🌠 功能需求
description: 提出需求或建议
title: '[Feature Request] '
labels: ['enhancement']
labels: [enhancement]
body:
- type: textarea
attributes:
label: '🥰 需求描述'
description: 请添加一个清晰且简洁的问题描述,阐述您希望通过这个功能需求解决的问题。
validations:
required: true
- type: textarea
attributes:
label: '🧐 解决方案'
description: 请清晰且简洁地描述您想要的解决方案。
validations:
required: true
- type: textarea
attributes:
label: '📝 补充信息'
description: 在这里添加关于问题的任何其他背景信息。
- type: textarea
attributes:
label: 🥰 需求描述
description: 请添加一个清晰且简洁的问题描述,阐述您希望通过这个功能需求解决的问题。
validations:
required: true
- type: textarea
attributes:
label: 🧐 解决方案
description: 请清晰且简洁地描述您想要的解决方案。
validations:
required: true
- type: textarea
attributes:
label: 📝 补充信息
description: 在这里添加关于问题的任何其他背景信息。

View File

@ -2,27 +2,27 @@
<!-- For change type, change [ ] to [x]. -->
- [ ] feat <!-- 引入新功能 | Introduce new features -->
- [ ] fix <!-- 修复 Bug | Fix a bug -->
- [ ] refactor <!-- 重构代码(既不修复 Bug 也不添加新功能) | Refactor code that neither fixes a bug nor adds a feature -->
- [ ] perf <!-- 提升性能的代码变更 | A code change that improves performance -->
- [ ] style <!-- 添加或更新不影响代码含义的样式文件 | Add or update style files that do not affect the meaning of the code -->
- [ ] test <!-- 添加缺失的测试或纠正现有的测试 | Adding missing tests or correcting existing tests -->
- [ ] docs <!-- 仅文档更新 | Documentation only changes -->
- [ ] ci <!-- 修改持续集成配置文件和脚本 | Changes to our CI configuration files and scripts -->
- [ ] chore <!-- 其他不修改 src 或 test 文件的变更 | Other changes that dont modify src or test files -->
- [ ] build <!-- 进行架构变更 | Make architectural changes -->
- [ ] feat <!-- 引入新功能 | Introduce new features -->
- [ ] fix <!-- 修复 Bug | Fix a bug -->
- [ ] refactor <!-- 重构代码(既不修复 Bug 也不添加新功能) | Refactor code that neither fixes a bug nor adds a feature -->
- [ ] perf <!-- 提升性能的代码变更 | A code change that improves performance -->
- [ ] style <!-- 添加或更新不影响代码含义的样式文件 | Add or update style files that do not affect the meaning of the code -->
- [ ] test <!-- 添加缺失的测试或纠正现有的测试 | Adding missing tests or correcting existing tests -->
- [ ] docs <!-- 仅文档更新 | Documentation only changes -->
- [ ] ci <!-- 修改持续集成配置文件和脚本 | Changes to our CI configuration files and scripts -->
- [ ] chore <!-- 其他不修改 src 或 test 文件的变更 | Other changes that dont modify src or test files -->
- [ ] build <!-- 进行架构变更 | Make architectural changes -->
#### 🔀 变更说明 | Description of Change
<!--
<!--
感谢您的 Pull Request ,请提供此 Pull Request 的变更说明
Thank you for your Pull Request. Please provide a description above.
-->
#### 📝 补充信息 | Additional Information
<!--
<!--
请添加与此 Pull Request 相关的补充信息
Add any other context about the Pull Request here.
-->

View File

@ -5,7 +5,7 @@
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
- package-ecosystem: npm # See documentation for possible values
directory: / # Location of package manifests
schedule:
interval: weekly

View File

@ -1,52 +1,45 @@
name: Publish Docker image
on:
workflow_dispatch:
release:
types: [published]
workflow_dispatch:
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
-
name: Check out the repo
uses: actions/checkout@v3
-
name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: yidadaa/chatgpt-next-web
tags: |
type=raw,value=latest
type=ref,event=tag
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: yidadaa/chatgpt-next-web
tags: |
type=raw,value=latest
type=ref,event=tag
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,15 +1,15 @@
name: Issue Translator
on:
issue_comment:
types: [created]
issues:
types: [opened]
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: false
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: false
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.

View File

@ -1,40 +1,40 @@
name: Upstream Sync
permissions:
contents: write
contents: write
on:
schedule:
- cron: "0 0 * * *" # every day
workflow_dispatch:
schedule:
- cron: '0 0 * * *' # every day
workflow_dispatch:
jobs:
sync_latest_from_upstream:
name: Sync latest commits from upstream repo
runs-on: ubuntu-latest
if: ${{ github.event.repository.fork }}
sync_latest_from_upstream:
name: Sync latest commits from upstream repo
runs-on: ubuntu-latest
if: ${{ github.event.repository.fork }}
steps:
# Step 1: run a standard checkout action
- name: Checkout target repo
uses: actions/checkout@v3
steps:
# Step 1: run a standard checkout action
- name: Checkout target repo
uses: actions/checkout@v3
# Step 2: run the sync action
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with:
upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web
upstream_sync_branch: main
target_sync_branch: sync-main
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
# Step 2: run the sync action
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with:
upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web
upstream_sync_branch: main
target_sync_branch: sync-main
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
# Set test_mode true to run tests instead of the true action!!
test_mode: false
# Set test_mode true to run tests instead of the true action!!
test_mode: false
- name: Sync check
if: failure()
run: |
echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次详细教程请查看https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0"
echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates"
exit 1
- name: Sync check
if: failure()
run: |
echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次详细教程请查看https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0"
echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates"
exit 1

View File

@ -11,7 +11,7 @@
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
<div align="center">
![主界面](./docs/images/cover.png)
</div>
@ -158,7 +158,6 @@ ChatGLM Api Key.
ChatGLM Api Url.
### `HIDE_USER_API_KEY` (可选)
如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。
@ -178,8 +177,9 @@ ChatGLM Api Url.
### `WHITE_WEBDAV_ENDPOINTS` (可选)
如果你想增加允许访问的webdav服务地址可以使用该选项格式要求
- 每一个地址必须是一个完整的 endpoint
> `https://xxxx/xxx`
> `https://xxxx/xxx`
- 多个地址以`,`相连
### `CUSTOM_MODELS` (可选)
@ -190,12 +190,13 @@ ChatGLM 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)`
在ByteDance的模式下支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
### `DEFAULT_MODEL` (可选)
@ -213,7 +214,6 @@ Stability API密钥
自定义的Stability API请求地址
## 开发
在开始写代码之前,需要在项目根目录新建一个 `.env.local` 文件,里面填入环境变量:

View File

@ -1,18 +1,18 @@
import { ApiPath } from "@/app/constant";
import { NextRequest } from "next/server";
import { handle as openaiHandler } from "../../openai";
import { handle as azureHandler } from "../../azure";
import { handle as googleHandler } from "../../google";
import { handle as anthropicHandler } from "../../anthropic";
import { handle as baiduHandler } from "../../baidu";
import { handle as bytedanceHandler } from "../../bytedance";
import { handle as alibabaHandler } from "../../alibaba";
import { handle as moonshotHandler } from "../../moonshot";
import { handle as stabilityHandler } from "../../stability";
import { handle as iflytekHandler } from "../../iflytek";
import { handle as xaiHandler } from "../../xai";
import { handle as chatglmHandler } from "../../glm";
import { handle as proxyHandler } from "../../proxy";
import type { NextRequest } from 'next/server';
import { ApiPath } from '@/app/constant';
import { handle as alibabaHandler } from '../../alibaba';
import { handle as anthropicHandler } from '../../anthropic';
import { handle as azureHandler } from '../../azure';
import { handle as baiduHandler } from '../../baidu';
import { handle as bytedanceHandler } from '../../bytedance';
import { handle as chatglmHandler } from '../../glm';
import { handle as googleHandler } from '../../google';
import { handle as iflytekHandler } from '../../iflytek';
import { handle as moonshotHandler } from '../../moonshot';
import { handle as openaiHandler } from '../../openai';
import { handle as proxyHandler } from '../../proxy';
import { handle as stabilityHandler } from '../../stability';
import { handle as xaiHandler } from '../../xai';
async function handle(
req: NextRequest,
@ -54,23 +54,23 @@ async function handle(
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const runtime = 'edge';
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
'arn1',
'bom1',
'cdg1',
'cle1',
'cpt1',
'dub1',
'fra1',
'gru1',
'hnd1',
'iad1',
'icn1',
'kix1',
'lhr1',
'pdx1',
'sfo1',
'sin1',
'syd1',
];

View File

@ -1,14 +1,15 @@
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { auth } from '@/app/api/auth';
import { getServerSideConfig } from '@/app/config/server';
import {
ALIBABA_BASE_URL,
ApiPath,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
} from '@/app/constant';
import { prettyObject } from '@/app/utils/format';
import { isModelAvailableInServer } from '@/app/utils/model';
import { NextResponse } from 'next/server';
const serverConfig = getServerSideConfig();
@ -16,10 +17,10 @@ export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Alibaba Route] params ", params);
console.log('[Alibaba Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const authResult = auth(req, ModelProvider.Qwen);
@ -33,7 +34,7 @@ export async function handle(
const response = await request(req);
return response;
} catch (e) {
console.error("[Alibaba] ", e);
console.error('[Alibaba] ', e);
return NextResponse.json(prettyObject(e));
}
}
@ -42,20 +43,20 @@ async function request(req: NextRequest) {
const controller = new AbortController();
// alibaba use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Alibaba, "");
const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Alibaba, '');
let baseUrl = serverConfig.alibabaUrl || ALIBABA_BASE_URL;
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -67,15 +68,15 @@ async function request(req: NextRequest) {
const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
"X-DashScope-SSE": req.headers.get("X-DashScope-SSE") ?? "disable",
'Content-Type': 'application/json',
'Authorization': req.headers.get('Authorization') ?? '',
'X-DashScope-SSE': req.headers.get('X-DashScope-SSE') ?? 'disable',
},
method: req.method,
body: req.body,
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -114,9 +115,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,

View File

@ -1,16 +1,17 @@
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { getServerSideConfig } from '@/app/config/server';
import {
ANTHROPIC_BASE_URL,
Anthropic,
ANTHROPIC_BASE_URL,
ApiPath,
ServiceProvider,
ModelProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth";
import { isModelAvailableInServer } from "@/app/utils/model";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
ServiceProvider,
} from '@/app/constant';
import { cloudflareAIGatewayUrl } from '@/app/utils/cloudflare';
import { prettyObject } from '@/app/utils/format';
import { isModelAvailableInServer } from '@/app/utils/model';
import { NextResponse } from 'next/server';
import { auth } from './auth';
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
@ -18,20 +19,20 @@ export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Anthropic Route] params ", params);
console.log('[Anthropic Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const subpath = params.path.join("/");
const subpath = params.path.join('/');
if (!ALLOWD_PATH.has(subpath)) {
console.log("[Anthropic Route] forbidden path ", subpath);
console.log('[Anthropic Route] forbidden path ', subpath);
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + subpath,
msg: `you are not allowed to request ${subpath}`,
},
{
status: 403,
@ -50,7 +51,7 @@ export async function handle(
const response = await request(req);
return response;
} catch (e) {
console.error("[Anthropic] ", e);
console.error('[Anthropic] ', e);
return NextResponse.json(prettyObject(e));
}
}
@ -60,28 +61,28 @@ const serverConfig = getServerSideConfig();
async function request(req: NextRequest) {
const controller = new AbortController();
let authHeaderName = "x-api-key";
let authValue =
req.headers.get(authHeaderName) ||
req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() ||
serverConfig.anthropicApiKey ||
"";
const authHeaderName = 'x-api-key';
const authValue
= req.headers.get(authHeaderName)
|| req.headers.get('Authorization')?.replaceAll('Bearer ', '').trim()
|| serverConfig.anthropicApiKey
|| '';
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, "");
const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, '');
let baseUrl =
serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
let baseUrl
= serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -95,20 +96,20 @@ async function request(req: NextRequest) {
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
"anthropic-dangerous-direct-browser-access": "true",
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'anthropic-dangerous-direct-browser-access': 'true',
[authHeaderName]: authValue,
"anthropic-version":
req.headers.get("anthropic-version") ||
serverConfig.anthropicApiVersion ||
Anthropic.Vision,
'anthropic-version':
req.headers.get('anthropic-version')
|| serverConfig.anthropicApiVersion
|| Anthropic.Vision,
},
method: req.method,
body: req.body,
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -155,9 +156,9 @@ async function request(req: NextRequest) {
// );
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,

View File

@ -1,6 +1,7 @@
import md5 from "spark-md5";
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { getServerSideConfig } from '@/app/config/server';
import { NextResponse } from 'next/server';
import md5 from 'spark-md5';
async function handle(req: NextRequest, res: NextResponse) {
const serverConfig = getServerSideConfig();
@ -9,7 +10,7 @@ async function handle(req: NextRequest, res: NextResponse) {
const storeHeaders = () => ({
Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
});
if (req.method === "POST") {
if (req.method === 'POST') {
const clonedBody = await req.text();
const hashedCode = md5.hash(clonedBody).trim();
const body: {
@ -21,9 +22,9 @@ async function handle(req: NextRequest, res: NextResponse) {
value: clonedBody,
};
try {
const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
const ttl = Number.parseInt(serverConfig.cloudflareKVTTL as string);
if (ttl > 60) {
body["expiration_ttl"] = ttl;
body.expiration_ttl = ttl;
}
} catch (e) {
console.error(e);
@ -31,13 +32,13 @@ async function handle(req: NextRequest, res: NextResponse) {
const res = await fetch(`${storeUrl()}/bulk`, {
headers: {
...storeHeaders(),
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
method: "PUT",
method: 'PUT',
body: JSON.stringify([body]),
});
const result = await res.json();
console.log("save data", result);
console.log('save data', result);
if (result?.success) {
return NextResponse.json(
{ code: 0, id: hashedCode, result },
@ -45,15 +46,15 @@ async function handle(req: NextRequest, res: NextResponse) {
);
}
return NextResponse.json(
{ error: true, msg: "Save data error" },
{ error: true, msg: 'Save data error' },
{ status: 400 },
);
}
if (req.method === "GET") {
const id = req?.nextUrl?.searchParams?.get("id");
if (req.method === 'GET') {
const id = req?.nextUrl?.searchParams?.get('id');
const res = await fetch(`${storeUrl()}/values/${id}`, {
headers: storeHeaders(),
method: "GET",
method: 'GET',
});
return new Response(res.body, {
status: res.status,
@ -62,7 +63,7 @@ async function handle(req: NextRequest, res: NextResponse) {
});
}
return NextResponse.json(
{ error: true, msg: "Invalid request" },
{ error: true, msg: 'Invalid request' },
{ status: 400 },
);
}
@ -70,4 +71,4 @@ async function handle(req: NextRequest, res: NextResponse) {
export const POST = handle;
export const GET = handle;
export const runtime = "edge";
export const runtime = 'edge';

View File

@ -1,55 +1,55 @@
import { NextRequest } from "next/server";
import { getServerSideConfig } from "../config/server";
import md5 from "spark-md5";
import { ACCESS_CODE_PREFIX, ModelProvider } from "../constant";
import type { NextRequest } from 'next/server';
import md5 from 'spark-md5';
import { getServerSideConfig } from '../config/server';
import { ACCESS_CODE_PREFIX, ModelProvider } from '../constant';
function getIP(req: NextRequest) {
let ip = req.ip ?? req.headers.get("x-real-ip");
const forwardedFor = req.headers.get("x-forwarded-for");
let ip = req.ip ?? req.headers.get('x-real-ip');
const forwardedFor = req.headers.get('x-forwarded-for');
if (!ip && forwardedFor) {
ip = forwardedFor.split(",").at(0) ?? "";
ip = forwardedFor.split(',').at(0) ?? '';
}
return ip;
}
function parseApiKey(bearToken: string) {
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
const token = bearToken.trim().replaceAll('Bearer ', '').trim();
const isApiKey = !token.startsWith(ACCESS_CODE_PREFIX);
return {
accessCode: isApiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
apiKey: isApiKey ? token : "",
accessCode: isApiKey ? '' : token.slice(ACCESS_CODE_PREFIX.length),
apiKey: isApiKey ? token : '',
};
}
export function auth(req: NextRequest, modelProvider: ModelProvider) {
const authToken = req.headers.get("Authorization") ?? "";
const authToken = req.headers.get('Authorization') ?? '';
// check if it is openai api key or user token
const { accessCode, apiKey } = parseApiKey(authToken);
const hashedCode = md5.hash(accessCode ?? "").trim();
const hashedCode = md5.hash(accessCode ?? '').trim();
const serverConfig = getServerSideConfig();
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
console.log("[Auth] got access code:", accessCode);
console.log("[Auth] hashed access code:", hashedCode);
console.log("[User IP] ", getIP(req));
console.log("[Time] ", new Date().toLocaleString());
console.log('[Auth] allowed hashed codes: ', [...serverConfig.codes]);
console.log('[Auth] got access code:', accessCode);
console.log('[Auth] hashed access code:', hashedCode);
console.log('[User IP] ', getIP(req));
console.log('[Time] ', new Date().toLocaleString());
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
return {
error: true,
msg: !accessCode ? "empty access code" : "wrong access code",
msg: !accessCode ? 'empty access code' : 'wrong access code',
};
}
if (serverConfig.hideUserApiKey && !!apiKey) {
return {
error: true,
msg: "you are not allowed to access with your own api key",
msg: 'you are not allowed to access with your own api key',
};
}
@ -89,8 +89,8 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
systemApiKey = serverConfig.moonshotApiKey;
break;
case ModelProvider.Iflytek:
systemApiKey =
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
systemApiKey
= `${serverConfig.iflytekApiKey}:${serverConfig.iflytekApiSecret}`;
break;
case ModelProvider.XAI:
systemApiKey = serverConfig.xaiApiKey;
@ -100,7 +100,7 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
break;
case ModelProvider.GPT:
default:
if (req.nextUrl.pathname.includes("azure/deployments")) {
if (req.nextUrl.pathname.includes('azure/deployments')) {
systemApiKey = serverConfig.azureApiKey;
} else {
systemApiKey = serverConfig.apiKey;
@ -108,13 +108,13 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
}
if (systemApiKey) {
console.log("[Auth] use system api key");
req.headers.set("Authorization", `Bearer ${systemApiKey}`);
console.log('[Auth] use system api key');
req.headers.set('Authorization', `Bearer ${systemApiKey}`);
} else {
console.log("[Auth] admin did not provide an api key");
console.log('[Auth] admin did not provide an api key');
}
} else {
console.log("[Auth] use user api key");
console.log('[Auth] use user api key');
}
return {

View File

@ -1,20 +1,21 @@
import { ModelProvider } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth";
import { requestOpenai } from "./common";
import type { NextRequest } from 'next/server';
import { ModelProvider } from '@/app/constant';
import { prettyObject } from '@/app/utils/format';
import { NextResponse } from 'next/server';
import { auth } from './auth';
import { requestOpenai } from './common';
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Azure Route] params ", params);
console.log('[Azure Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const subpath = params.path.join("/");
const subpath = params.path.join('/');
const authResult = auth(req, ModelProvider.GPT);
if (authResult.error) {
@ -26,7 +27,7 @@ export async function handle(
try {
return await requestOpenai(req);
} catch (e) {
console.error("[Azure] ", e);
console.error('[Azure] ', e);
return NextResponse.json(prettyObject(e));
}
}

View File

@ -1,15 +1,16 @@
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { auth } from '@/app/api/auth';
import { getServerSideConfig } from '@/app/config/server';
import {
BAIDU_BASE_URL,
ApiPath,
BAIDU_BASE_URL,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
import { getAccessToken } from "@/app/utils/baidu";
} from '@/app/constant';
import { getAccessToken } from '@/app/utils/baidu';
import { prettyObject } from '@/app/utils/format';
import { isModelAvailableInServer } from '@/app/utils/model';
import { NextResponse } from 'next/server';
const serverConfig = getServerSideConfig();
@ -17,10 +18,10 @@ export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Baidu Route] params ", params);
console.log('[Baidu Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const authResult = auth(req, ModelProvider.Ernie);
@ -46,7 +47,7 @@ export async function handle(
const response = await request(req);
return response;
} catch (e) {
console.error("[Baidu] ", e);
console.error('[Baidu] ', e);
return NextResponse.json(prettyObject(e));
}
}
@ -54,20 +55,20 @@ export async function handle(
async function request(req: NextRequest) {
const controller = new AbortController();
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Baidu, "");
const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Baidu, '');
let baseUrl = serverConfig.baiduUrl || BAIDU_BASE_URL;
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -84,13 +85,13 @@ async function request(req: NextRequest) {
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
method: req.method,
body: req.body,
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -129,9 +130,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,

View File

@ -1,14 +1,15 @@
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { auth } from '@/app/api/auth';
import { getServerSideConfig } from '@/app/config/server';
import {
BYTEDANCE_BASE_URL,
ApiPath,
BYTEDANCE_BASE_URL,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
} from '@/app/constant';
import { prettyObject } from '@/app/utils/format';
import { isModelAvailableInServer } from '@/app/utils/model';
import { NextResponse } from 'next/server';
const serverConfig = getServerSideConfig();
@ -16,10 +17,10 @@ export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[ByteDance Route] params ", params);
console.log('[ByteDance Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const authResult = auth(req, ModelProvider.Doubao);
@ -33,7 +34,7 @@ export async function handle(
const response = await request(req);
return response;
} catch (e) {
console.error("[ByteDance] ", e);
console.error('[ByteDance] ', e);
return NextResponse.json(prettyObject(e));
}
}
@ -41,20 +42,20 @@ export async function handle(
async function request(req: NextRequest) {
const controller = new AbortController();
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ByteDance, "");
const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ByteDance, '');
let baseUrl = serverConfig.bytedanceUrl || BYTEDANCE_BASE_URL;
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -67,14 +68,14 @@ async function request(req: NextRequest) {
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
'Content-Type': 'application/json',
'Authorization': req.headers.get('Authorization') ?? '',
},
method: req.method,
body: req.body,
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -114,9 +115,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,

View File

@ -1,47 +1,48 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../config/server";
import { OPENAI_BASE_URL, ServiceProvider } from "../constant";
import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
import { getModelProvider, isModelAvailableInServer } from "../utils/model";
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { getServerSideConfig } from '../config/server';
import { OPENAI_BASE_URL, ServiceProvider } from '../constant';
import { cloudflareAIGatewayUrl } from '../utils/cloudflare';
import { getModelProvider, isModelAvailableInServer } from '../utils/model';
const serverConfig = getServerSideConfig();
export async function requestOpenai(req: NextRequest) {
const controller = new AbortController();
const isAzure = req.nextUrl.pathname.includes("azure/deployments");
const isAzure = req.nextUrl.pathname.includes('azure/deployments');
var authValue,
authHeaderName = "";
let authValue;
let authHeaderName = '';
if (isAzure) {
authValue =
req.headers
.get("Authorization")
authValue
= req.headers
.get('Authorization')
?.trim()
.replaceAll("Bearer ", "")
.trim() ?? "";
.replaceAll('Bearer ', '')
.trim() ?? '';
authHeaderName = "api-key";
authHeaderName = 'api-key';
} else {
authValue = req.headers.get("Authorization") ?? "";
authHeaderName = "Authorization";
authValue = req.headers.get('Authorization') ?? '';
authHeaderName = 'Authorization';
}
let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");
let path = `${req.nextUrl.pathname}`.replaceAll('/api/openai/', '');
let baseUrl =
(isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
let baseUrl
= (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -51,30 +52,30 @@ export async function requestOpenai(req: NextRequest) {
);
if (isAzure) {
const azureApiVersion =
req?.nextUrl?.searchParams?.get("api-version") ||
serverConfig.azureApiVersion;
baseUrl = baseUrl.split("/deployments").shift() as string;
const azureApiVersion
= req?.nextUrl?.searchParams?.get('api-version')
|| serverConfig.azureApiVersion;
baseUrl = baseUrl.split('/deployments').shift() as string;
path = `${req.nextUrl.pathname.replaceAll(
"/api/azure/",
"",
'/api/azure/',
'',
)}?api-version=${azureApiVersion}`;
// Forward compatibility:
// if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL
// then using default '{deploy-id}'
if (serverConfig.customModels && serverConfig.azureUrl) {
const modelName = path.split("/")[1];
let realDeployName = "";
const modelName = path.split('/')[1];
let realDeployName = '';
serverConfig.customModels
.split(",")
.filter((v) => !!v && !v.startsWith("-") && v.includes(modelName))
.split(',')
.filter(v => !!v && !v.startsWith('-') && v.includes(modelName))
.forEach((m) => {
const [fullName, displayName] = m.split("=");
const [fullName, displayName] = m.split('=');
const [_, providerName] = getModelProvider(fullName);
if (providerName === "azure" && !displayName) {
const [_, deployId] = (serverConfig?.azureUrl ?? "").split(
"deployments/",
if (providerName === 'azure' && !displayName) {
const [_, deployId] = (serverConfig?.azureUrl ?? '').split(
'deployments/',
);
if (deployId) {
realDeployName = deployId;
@ -82,29 +83,29 @@ export async function requestOpenai(req: NextRequest) {
}
});
if (realDeployName) {
console.log("[Replace with DeployId", realDeployName);
console.log('[Replace with DeployId', realDeployName);
path = path.replaceAll(modelName, realDeployName);
}
}
}
const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
console.log("fetchUrl", fetchUrl);
console.log('fetchUrl', fetchUrl);
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
[authHeaderName]: authValue,
...(serverConfig.openaiOrgId && {
"OpenAI-Organization": serverConfig.openaiOrgId,
'OpenAI-Organization': serverConfig.openaiOrgId,
}),
},
method: req.method,
body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -122,8 +123,8 @@ export async function requestOpenai(req: NextRequest) {
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.OpenAI as string,
) ||
isModelAvailableInServer(
)
|| isModelAvailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Azure as string,
@ -140,7 +141,7 @@ export async function requestOpenai(req: NextRequest) {
);
}
} catch (e) {
console.error("[OpenAI] gpt4 filter", e);
console.error('[OpenAI] gpt4 filter', e);
}
}
@ -148,33 +149,33 @@ export async function requestOpenai(req: NextRequest) {
const res = await fetch(fetchUrl, fetchOptions);
// Extract the OpenAI-Organization header from the response
const openaiOrganizationHeader = res.headers.get("OpenAI-Organization");
const openaiOrganizationHeader = res.headers.get('OpenAI-Organization');
// Check if serverConfig.openaiOrgId is defined and not an empty string
if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") {
if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== '') {
// If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present
console.log("[Org ID]", openaiOrganizationHeader);
console.log('[Org ID]', openaiOrganizationHeader);
} else {
console.log("[Org ID] is not set up.");
console.log('[Org ID] is not set up.');
}
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
// Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
// Also, this is to prevent the header from being sent to the client
if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") {
newHeaders.delete("OpenAI-Organization");
if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === '') {
newHeaders.delete('OpenAI-Organization');
}
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
// So if the streaming is disabled, we need to remove the content-encoding header
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
// The browser will try to decode the response with brotli and fail
newHeaders.delete("content-encoding");
newHeaders.delete('content-encoding');
return new Response(res.body, {
status: res.status,

View File

@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { NextResponse } from 'next/server';
import { getServerSideConfig } from "../../config/server";
import { getServerSideConfig } from '../../config/server';
const serverConfig = getServerSideConfig();
@ -27,4 +27,4 @@ async function handle() {
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const runtime = 'edge';

View File

@ -1,14 +1,15 @@
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { auth } from '@/app/api/auth';
import { getServerSideConfig } from '@/app/config/server';
import {
CHATGLM_BASE_URL,
ApiPath,
CHATGLM_BASE_URL,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
} from '@/app/constant';
import { prettyObject } from '@/app/utils/format';
import { isModelAvailableInServer } from '@/app/utils/model';
import { NextResponse } from 'next/server';
const serverConfig = getServerSideConfig();
@ -16,10 +17,10 @@ export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[GLM Route] params ", params);
console.log('[GLM Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const authResult = auth(req, ModelProvider.ChatGLM);
@ -33,7 +34,7 @@ export async function handle(
const response = await request(req);
return response;
} catch (e) {
console.error("[GLM] ", e);
console.error('[GLM] ', e);
return NextResponse.json(prettyObject(e));
}
}
@ -42,20 +43,20 @@ async function request(req: NextRequest) {
const controller = new AbortController();
// alibaba use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, "");
const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, '');
let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL;
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -65,17 +66,17 @@ async function request(req: NextRequest) {
);
const fetchUrl = `${baseUrl}${path}`;
console.log("[Fetch Url] ", fetchUrl);
console.log('[Fetch Url] ', fetchUrl);
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
'Content-Type': 'application/json',
'Authorization': req.headers.get('Authorization') ?? '',
},
method: req.method,
body: req.body,
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -114,9 +115,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,

View File

@ -1,8 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth";
import { getServerSideConfig } from "@/app/config/server";
import { ApiPath, GEMINI_BASE_URL, ModelProvider } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import type { NextRequest } from 'next/server';
import { getServerSideConfig } from '@/app/config/server';
import { ApiPath, GEMINI_BASE_URL, ModelProvider } from '@/app/constant';
import { prettyObject } from '@/app/utils/format';
import { NextResponse } from 'next/server';
import { auth } from './auth';
const serverConfig = getServerSideConfig();
@ -10,10 +11,10 @@ export async function handle(
req: NextRequest,
{ params }: { params: { provider: string; path: string[] } },
) {
console.log("[Google Route] params ", params);
console.log('[Google Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const authResult = auth(req, ModelProvider.GeminiPro);
@ -23,11 +24,11 @@ export async function handle(
});
}
const bearToken =
req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || "";
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
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;
const apiKey = token || serverConfig.googleApiKey;
if (!apiKey) {
return NextResponse.json(
@ -44,7 +45,7 @@ export async function handle(
const response = await request(req, apiKey);
return response;
} catch (e) {
console.error("[Google] ", e);
console.error('[Google] ', e);
return NextResponse.json(prettyObject(e));
}
}
@ -52,20 +53,20 @@ export async function handle(
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const runtime = 'edge';
export const preferredRegion = [
"bom1",
"cle1",
"cpt1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"pdx1",
"sfo1",
"sin1",
"syd1",
'bom1',
'cle1',
'cpt1',
'gru1',
'hnd1',
'iad1',
'icn1',
'kix1',
'pdx1',
'sfo1',
'sin1',
'syd1',
];
async function request(req: NextRequest, apiKey: string) {
@ -73,18 +74,18 @@ async function request(req: NextRequest, apiKey: string) {
let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, "");
const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, '');
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -93,24 +94,24 @@ async function request(req: NextRequest, apiKey: string) {
10 * 60 * 1000,
);
const fetchUrl = `${baseUrl}${path}${
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : ""
req?.nextUrl?.searchParams?.get('alt') === 'sse' ? '?alt=sse' : ''
}`;
console.log("[Fetch Url] ", fetchUrl);
console.log('[Fetch Url] ', fetchUrl);
const fetchOptions: RequestInit = {
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 ", ""),
'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,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -118,9 +119,9 @@ async function request(req: NextRequest, apiKey: string) {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,

View File

@ -1,14 +1,15 @@
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { auth } from '@/app/api/auth';
import { getServerSideConfig } from '@/app/config/server';
import {
IFLYTEK_BASE_URL,
ApiPath,
IFLYTEK_BASE_URL,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
} from '@/app/constant';
import { prettyObject } from '@/app/utils/format';
import { isModelAvailableInServer } from '@/app/utils/model';
import { NextResponse } from 'next/server';
// iflytek
const serverConfig = getServerSideConfig();
@ -17,10 +18,10 @@ export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Iflytek Route] params ", params);
console.log('[Iflytek Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const authResult = auth(req, ModelProvider.Iflytek);
@ -34,7 +35,7 @@ export async function handle(
const response = await request(req);
return response;
} catch (e) {
console.error("[Iflytek] ", e);
console.error('[Iflytek] ', e);
return NextResponse.json(prettyObject(e));
}
}
@ -43,20 +44,20 @@ async function request(req: NextRequest) {
const controller = new AbortController();
// iflytek use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, "");
const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, '');
let baseUrl = serverConfig.iflytekUrl || IFLYTEK_BASE_URL;
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -68,14 +69,14 @@ async function request(req: NextRequest) {
const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
'Content-Type': 'application/json',
'Authorization': req.headers.get('Authorization') ?? '',
},
method: req.method,
body: req.body,
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -114,9 +115,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,

View File

@ -1,14 +1,15 @@
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { auth } from '@/app/api/auth';
import { getServerSideConfig } from '@/app/config/server';
import {
MOONSHOT_BASE_URL,
ApiPath,
ModelProvider,
MOONSHOT_BASE_URL,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
} from '@/app/constant';
import { prettyObject } from '@/app/utils/format';
import { isModelAvailableInServer } from '@/app/utils/model';
import { NextResponse } from 'next/server';
const serverConfig = getServerSideConfig();
@ -16,10 +17,10 @@ export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Moonshot Route] params ", params);
console.log('[Moonshot Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const authResult = auth(req, ModelProvider.Moonshot);
@ -33,7 +34,7 @@ export async function handle(
const response = await request(req);
return response;
} catch (e) {
console.error("[Moonshot] ", e);
console.error('[Moonshot] ', e);
return NextResponse.json(prettyObject(e));
}
}
@ -42,20 +43,20 @@ async function request(req: NextRequest) {
const controller = new AbortController();
// alibaba use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Moonshot, "");
const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Moonshot, '');
let baseUrl = serverConfig.moonshotUrl || MOONSHOT_BASE_URL;
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -67,14 +68,14 @@ async function request(req: NextRequest) {
const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
'Content-Type': 'application/json',
'Authorization': req.headers.get('Authorization') ?? '',
},
method: req.method,
body: req.body,
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -113,9 +114,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,

View File

@ -1,10 +1,11 @@
import { type OpenAIListModelResponse } from "@/app/client/platforms/openai";
import { getServerSideConfig } from "@/app/config/server";
import { ModelProvider, OpenaiPath } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth";
import { requestOpenai } from "./common";
import type { OpenAIListModelResponse } from '@/app/client/platforms/openai';
import type { NextRequest } from 'next/server';
import { getServerSideConfig } from '@/app/config/server';
import { ModelProvider, OpenaiPath } from '@/app/constant';
import { prettyObject } from '@/app/utils/format';
import { NextResponse } from 'next/server';
import { auth } from './auth';
import { requestOpenai } from './common';
const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
@ -13,9 +14,9 @@ function getModels(remoteModelRes: OpenAIListModelResponse) {
if (config.disableGPT4) {
remoteModelRes.data = remoteModelRes.data.filter(
(m) =>
!(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o") || m.id.startsWith("o1")) ||
m.id.startsWith("gpt-4o-mini"),
m =>
!(m.id.startsWith('gpt-4') || m.id.startsWith('chatgpt-4o') || m.id.startsWith('o1'))
|| m.id.startsWith('gpt-4o-mini'),
);
}
@ -26,20 +27,20 @@ export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[OpenAI Route] params ", params);
console.log('[OpenAI Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const subpath = params.path.join("/");
const subpath = params.path.join('/');
if (!ALLOWED_PATH.has(subpath)) {
console.log("[OpenAI Route] forbidden path ", subpath);
console.log('[OpenAI Route] forbidden path ', subpath);
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + subpath,
msg: `you are not allowed to request ${subpath}`,
},
{
status: 403,
@ -68,7 +69,7 @@ export async function handle(
return response;
} catch (e) {
console.error("[OpenAI] ", e);
console.error('[OpenAI] ', e);
return NextResponse.json(prettyObject(e));
}
}

View File

@ -1,32 +1,33 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { getServerSideConfig } from '@/app/config/server';
import { NextResponse } from 'next/server';
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Proxy Route] params ", params);
console.log('[Proxy Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const serverConfig = getServerSideConfig();
// remove path params from searchParams
req.nextUrl.searchParams.delete("path");
req.nextUrl.searchParams.delete("provider");
req.nextUrl.searchParams.delete('path');
req.nextUrl.searchParams.delete('provider');
const subpath = params.path.join("/");
const subpath = params.path.join('/');
const fetchUrl = `${req.headers.get(
"x-base-url",
'x-base-url',
)}/${subpath}?${req.nextUrl.searchParams.toString()}`;
const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
const skipHeaders = ['connection', 'host', 'origin', 'referer', 'cookie'];
const headers = new Headers(
Array.from(req.headers.entries()).filter((item) => {
if (
item[0].indexOf("x-") > -1 ||
item[0].indexOf("sec-") > -1 ||
skipHeaders.includes(item[0])
item[0].includes('x-')
|| item[0].includes('sec-')
|| skipHeaders.includes(item[0])
) {
return false;
}
@ -34,16 +35,16 @@ export async function handle(
}),
);
// if dalle3 use openai api key
const baseUrl = req.headers.get("x-base-url");
if (baseUrl?.includes("api.openai.com")) {
if (!serverConfig.apiKey) {
return NextResponse.json(
{ error: "OpenAI API key not configured" },
{ status: 500 },
);
}
headers.set("Authorization", `Bearer ${serverConfig.apiKey}`);
const baseUrl = req.headers.get('x-base-url');
if (baseUrl?.includes('api.openai.com')) {
if (!serverConfig.apiKey) {
return NextResponse.json(
{ error: 'OpenAI API key not configured' },
{ status: 500 },
);
}
headers.set('Authorization', `Bearer ${serverConfig.apiKey}`);
}
const controller = new AbortController();
const fetchOptions: RequestInit = {
@ -51,9 +52,9 @@ export async function handle(
method: req.method,
body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -68,15 +69,15 @@ export async function handle(
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
// So if the streaming is disabled, we need to remove the content-encoding header
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
// The browser will try to decode the response with brotli and fail
newHeaders.delete("content-encoding");
newHeaders.delete('content-encoding');
return new Response(res.body, {
status: res.status,

View File

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

View File

@ -1,9 +1,10 @@
import { getServerSideConfig } from "@/app/config/server";
import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { getHeader } from "@/app/utils/tencent";
import type { NextRequest } from 'next/server';
import { auth } from '@/app/api/auth';
import { getServerSideConfig } from '@/app/config/server';
import { ModelProvider, TENCENT_BASE_URL } from '@/app/constant';
import { prettyObject } from '@/app/utils/format';
import { getHeader } from '@/app/utils/tencent';
import { NextResponse } from 'next/server';
const serverConfig = getServerSideConfig();
@ -11,10 +12,10 @@ async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Tencent Route] params ", params);
console.log('[Tencent Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const authResult = auth(req, ModelProvider.Hunyuan);
@ -28,7 +29,7 @@ async function handle(
const response = await request(req);
return response;
} catch (e) {
console.error("[Tencent] ", e);
console.error('[Tencent] ', e);
return NextResponse.json(prettyObject(e));
}
}
@ -36,25 +37,25 @@ async function handle(
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const runtime = 'edge';
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
'arn1',
'bom1',
'cdg1',
'cle1',
'cpt1',
'dub1',
'fra1',
'gru1',
'hnd1',
'iad1',
'icn1',
'kix1',
'lhr1',
'pdx1',
'sfo1',
'sin1',
'syd1',
];
async function request(req: NextRequest) {
@ -62,15 +63,15 @@ async function request(req: NextRequest) {
let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Base Url]", baseUrl);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -91,9 +92,9 @@ async function request(req: NextRequest) {
headers,
method: req.method,
body,
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -102,9 +103,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,

View File

@ -1,22 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
async function handle(
req: NextRequest,
{ params }: { params: { action: string; key: string[] } },
) {
const requestUrl = new URL(req.url);
const endpoint = requestUrl.searchParams.get("endpoint");
const endpoint = requestUrl.searchParams.get('endpoint');
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const [...key] = params.key;
// only allow to request to *.upstash.io
if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) {
if (!endpoint || !new URL(endpoint).hostname.endsWith('.upstash.io')) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + params.key.join("/"),
msg: `you are not allowed to request ${params.key.join('/')}`,
},
{
status: 403,
@ -25,12 +26,12 @@ async function handle(
}
// only allow upstash get and set method
if (params.action !== "get" && params.action !== "set") {
console.log("[Upstash Route] forbidden action ", params.action);
if (params.action !== 'get' && params.action !== 'set') {
console.log('[Upstash Route] forbidden action ', params.action);
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + params.action,
msg: `you are not allowed to request ${params.action}`,
},
{
status: 403,
@ -38,27 +39,27 @@ async function handle(
);
}
const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`;
const targetUrl = `${endpoint}/${params.action}/${params.key.join('/')}`;
const method = req.method;
const shouldNotHaveBody = ["get", "head"].includes(
method?.toLowerCase() ?? "",
const shouldNotHaveBody = ['get', 'head'].includes(
method?.toLowerCase() ?? '',
);
const fetchOptions: RequestInit = {
headers: {
authorization: req.headers.get("authorization") ?? "",
authorization: req.headers.get('authorization') ?? '',
},
body: shouldNotHaveBody ? null : req.body,
method,
// @ts-ignore
duplex: "half",
duplex: 'half',
};
console.log("[Upstash Proxy]", targetUrl, fetchOptions);
console.log('[Upstash Proxy]', targetUrl, fetchOptions);
const fetchResult = await fetch(targetUrl, fetchOptions);
console.log("[Any Proxy]", targetUrl, {
console.log('[Any Proxy]', targetUrl, {
status: fetchResult.status,
statusText: fetchResult.statusText,
});
@ -70,4 +71,4 @@ export const POST = handle;
export const GET = handle;
export const OPTIONS = handle;
export const runtime = "edge";
export const runtime = 'edge';

View File

@ -1,47 +1,48 @@
import { NextRequest, NextResponse } from "next/server";
import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant";
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { getServerSideConfig } from '@/app/config/server';
import { NextResponse } from 'next/server';
import { internalAllowedWebDavEndpoints, STORAGE_KEY } from '../../../constant';
const config = getServerSideConfig();
const mergedAllowedWebDavEndpoints = [
...internalAllowedWebDavEndpoints,
...config.allowedWebDavEndpoints,
].filter((domain) => Boolean(domain.trim()));
].filter(domain => Boolean(domain.trim()));
const normalizeUrl = (url: string) => {
function normalizeUrl(url: string) {
try {
return new URL(url);
} catch (err) {
return null;
}
};
}
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const folder = STORAGE_KEY;
const fileName = `${folder}/backup.json`;
const requestUrl = new URL(req.url);
let endpoint = requestUrl.searchParams.get("endpoint");
let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method;
let endpoint = requestUrl.searchParams.get('endpoint');
const proxy_method = requestUrl.searchParams.get('proxy_method') || req.method;
// Validate the endpoint to prevent potential SSRF attacks
if (
!endpoint ||
!mergedAllowedWebDavEndpoints.some((allowedEndpoint) => {
!endpoint
|| !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => {
const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
const normalizedEndpoint = normalizeUrl(endpoint as string);
return (
normalizedEndpoint &&
normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
normalizedEndpoint.pathname.startsWith(
normalizedEndpoint
&& normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname
&& normalizedEndpoint.pathname.startsWith(
normalizedAllowedEndpoint.pathname,
)
);
@ -50,7 +51,7 @@ async function handle(
return NextResponse.json(
{
error: true,
msg: "Invalid endpoint",
msg: 'Invalid endpoint',
},
{
status: 400,
@ -58,23 +59,23 @@ async function handle(
);
}
if (!endpoint?.endsWith("/")) {
endpoint += "/";
if (!endpoint?.endsWith('/')) {
endpoint += '/';
}
const endpointPath = params.path.join("/");
const endpointPath = params.path.join('/');
const targetPath = `${endpoint}${endpointPath}`;
// only allow MKCOL, GET, PUT
if (
proxy_method !== "MKCOL" &&
proxy_method !== "GET" &&
proxy_method !== "PUT"
proxy_method !== 'MKCOL'
&& proxy_method !== 'GET'
&& proxy_method !== 'PUT'
) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
msg: `you are not allowed to request ${targetPath}`,
},
{
status: 403,
@ -83,11 +84,11 @@ async function handle(
}
// for MKCOL request, only allow request ${folder}
if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) {
if (proxy_method === 'MKCOL' && !targetPath.endsWith(folder)) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
msg: `you are not allowed to request ${targetPath}`,
},
{
status: 403,
@ -96,11 +97,11 @@ async function handle(
}
// for GET request, only allow request ending with fileName
if (proxy_method === "GET" && !targetPath.endsWith(fileName)) {
if (proxy_method === 'GET' && !targetPath.endsWith(fileName)) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
msg: `you are not allowed to request ${targetPath}`,
},
{
status: 403,
@ -109,11 +110,11 @@ async function handle(
}
// for PUT request, only allow request ending with fileName
if (proxy_method === "PUT" && !targetPath.endsWith(fileName)) {
if (proxy_method === 'PUT' && !targetPath.endsWith(fileName)) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
msg: `you are not allowed to request ${targetPath}`,
},
{
status: 403,
@ -124,19 +125,19 @@ async function handle(
const targetUrl = targetPath;
const method = proxy_method || req.method;
const shouldNotHaveBody = ["get", "head"].includes(
method?.toLowerCase() ?? "",
const shouldNotHaveBody = ['get', 'head'].includes(
method?.toLowerCase() ?? '',
);
const fetchOptions: RequestInit = {
headers: {
authorization: req.headers.get("authorization") ?? "",
authorization: req.headers.get('authorization') ?? '',
},
body: shouldNotHaveBody ? null : req.body,
redirect: "manual",
redirect: 'manual',
method,
// @ts-ignore
duplex: "half",
duplex: 'half',
};
let fetchResult;
@ -145,10 +146,10 @@ async function handle(
fetchResult = await fetch(targetUrl, fetchOptions);
} finally {
console.log(
"[Any Proxy]",
'[Any Proxy]',
targetUrl,
{
method: method,
method,
},
{
status: fetchResult?.status,
@ -164,4 +165,4 @@ export const PUT = handle;
export const GET = handle;
export const OPTIONS = handle;
export const runtime = "edge";
export const runtime = 'edge';

View File

@ -1,14 +1,15 @@
import { getServerSideConfig } from "@/app/config/server";
import type { NextRequest } from 'next/server';
import { auth } from '@/app/api/auth';
import { getServerSideConfig } from '@/app/config/server';
import {
XAI_BASE_URL,
ApiPath,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
XAI_BASE_URL,
} from '@/app/constant';
import { prettyObject } from '@/app/utils/format';
import { isModelAvailableInServer } from '@/app/utils/model';
import { NextResponse } from 'next/server';
const serverConfig = getServerSideConfig();
@ -16,10 +17,10 @@ export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[XAI Route] params ", params);
console.log('[XAI Route] params ', params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const authResult = auth(req, ModelProvider.XAI);
@ -33,7 +34,7 @@ export async function handle(
const response = await request(req);
return response;
} catch (e) {
console.error("[XAI] ", e);
console.error('[XAI] ', e);
return NextResponse.json(prettyObject(e));
}
}
@ -42,20 +43,20 @@ async function request(req: NextRequest) {
const controller = new AbortController();
// alibaba use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, "");
const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, '');
let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL;
if (!baseUrl.startsWith("http")) {
if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log('[Proxy] ', path);
console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout(
() => {
@ -67,14 +68,14 @@ async function request(req: NextRequest) {
const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
'Content-Type': 'application/json',
'Authorization': req.headers.get('Authorization') ?? '',
},
method: req.method,
body: req.body,
redirect: "manual",
redirect: 'manual',
// @ts-ignore
duplex: "half",
duplex: 'half',
signal: controller.signal,
};
@ -113,9 +114,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
newHeaders.delete('www-authenticate');
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, {
status: res.status,

View File

@ -1,37 +1,40 @@
import { getClientConfig } from "../config/client";
import type {
ChatMessage,
ChatMessageTool,
ModelType,
} from '../store';
import type { DalleRequestPayload } from './platforms/openai';
import { getClientConfig } from '../config/client';
import {
ACCESS_CODE_PREFIX,
ModelProvider,
ServiceProvider,
} from "../constant";
} from '../constant';
import {
ChatMessageTool,
ChatMessage,
ModelType,
useAccessStore,
useChatStore,
} from "../store";
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
import { GeminiProApi } from "./platforms/google";
import { ClaudeApi } from "./platforms/anthropic";
import { ErnieApi } from "./platforms/baidu";
import { DoubaoApi } from "./platforms/bytedance";
import { QwenApi } from "./platforms/alibaba";
import { HunyuanApi } from "./platforms/tencent";
import { MoonshotApi } from "./platforms/moonshot";
import { SparkApi } from "./platforms/iflytek";
import { XAIApi } from "./platforms/xai";
import { ChatGLMApi } from "./platforms/glm";
} from '../store';
import { QwenApi } from './platforms/alibaba';
import { ClaudeApi } from './platforms/anthropic';
import { ErnieApi } from './platforms/baidu';
import { DoubaoApi } from './platforms/bytedance';
import { ChatGLMApi } from './platforms/glm';
import { GeminiProApi } from './platforms/google';
import { SparkApi } from './platforms/iflytek';
import { MoonshotApi } from './platforms/moonshot';
import { ChatGPTApi } from './platforms/openai';
import { HunyuanApi } from './platforms/tencent';
import { XAIApi } from './platforms/xai';
export const ROLES = ["system", "user", "assistant"] as const;
export const ROLES = ['system', 'user', 'assistant'] as const;
export type MessageRole = (typeof ROLES)[number];
export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
export const TTSModels = ["tts-1", "tts-1-hd"] as const;
export const Models = ['gpt-3.5-turbo', 'gpt-4'] as const;
export const TTSModels = ['tts-1', 'tts-1-hd'] as const;
export type ChatModel = ModelType;
export interface MultimodalContent {
type: "text" | "image_url";
type: 'text' | 'image_url';
text?: string;
image_url?: {
url: string;
@ -51,9 +54,9 @@ export interface LLMConfig {
stream?: boolean;
presence_penalty?: number;
frequency_penalty?: number;
size?: DalleRequestPayload["size"];
quality?: DalleRequestPayload["quality"];
style?: DalleRequestPayload["style"];
size?: DalleRequestPayload['size'];
quality?: DalleRequestPayload['quality'];
style?: DalleRequestPayload['style'];
}
export interface SpeechOptions {
@ -104,7 +107,7 @@ export abstract class LLMApi {
abstract models(): Promise<LLMModel[]>;
}
type ProviderName = "openai" | "azure" | "claude" | "palm";
type ProviderName = 'openai' | 'azure' | 'claude' | 'palm';
interface Model {
name: string;
@ -173,24 +176,24 @@ export class ClientApi {
async share(messages: ChatMessage[], avatarUrl: string | null = null) {
const msgs = messages
.map((m) => ({
from: m.role === "user" ? "human" : "gpt",
.map(m => ({
from: m.role === 'user' ? 'human' : 'gpt',
value: m.content,
}))
.concat([
{
from: "human",
from: 'human',
value:
"Share from [NextChat]: https://github.com/Yidadaa/ChatGPT-Next-Web",
'Share from [NextChat]: https://github.com/Yidadaa/ChatGPT-Next-Web',
},
]);
// 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用
// Please do not modify this message
console.log("[Share]", messages, msgs);
console.log('[Share]', messages, msgs);
const clientConfig = getClientConfig();
const proxyUrl = "/sharegpt";
const rawUrl = "https://sharegpt.com/api/conversations";
const proxyUrl = '/sharegpt';
const rawUrl = 'https://sharegpt.com/api/conversations';
const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl;
const res = await fetch(shareUrl, {
body: JSON.stringify({
@ -198,13 +201,13 @@ export class ClientApi {
items: msgs,
}),
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
method: "POST",
method: 'POST',
});
const resJson = await res.json();
console.log("[Share]", resJson);
console.log('[Share]', resJson);
if (resJson.id) {
return `https://shareg.pt/${resJson.id}`;
}
@ -216,8 +219,8 @@ export function getBearerToken(
noBearer: boolean = false,
): string {
return validString(apiKey)
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
: "";
? `${noBearer ? '' : 'Bearer '}${apiKey.trim()}`
: '';
}
export function validString(x: string): boolean {
@ -230,8 +233,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
let headers: Record<string, string> = {};
if (!ignoreHeaders) {
headers = {
"Content-Type": "application/json",
Accept: "application/json",
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
@ -253,24 +256,24 @@ export function getHeaders(ignoreHeaders: boolean = false) {
const apiKey = isGoogle
? accessStore.googleApiKey
: isAzure
? accessStore.azureApiKey
: isAnthropic
? accessStore.anthropicApiKey
: isByteDance
? accessStore.bytedanceApiKey
: isAlibaba
? accessStore.alibabaApiKey
: isMoonshot
? accessStore.moonshotApiKey
: isXAI
? accessStore.xaiApiKey
: isChatGLM
? accessStore.chatglmApiKey
: isIflytek
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
: ""
: accessStore.openaiApiKey;
? accessStore.azureApiKey
: isAnthropic
? accessStore.anthropicApiKey
: isByteDance
? accessStore.bytedanceApiKey
: isAlibaba
? accessStore.alibabaApiKey
: isMoonshot
? accessStore.moonshotApiKey
: isXAI
? accessStore.xaiApiKey
: isChatGLM
? accessStore.chatglmApiKey
: isIflytek
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
? `${accessStore.iflytekApiKey}:${accessStore.iflytekApiSecret}`
: ''
: accessStore.openaiApiKey;
return {
isGoogle,
isAzure,
@ -289,12 +292,12 @@ export function getHeaders(ignoreHeaders: boolean = false) {
function getAuthHeader(): string {
return isAzure
? "api-key"
? 'api-key'
: isAnthropic
? "x-api-key"
: isGoogle
? "x-goog-api-key"
: "Authorization";
? 'x-api-key'
: isGoogle
? 'x-goog-api-key'
: 'Authorization';
}
const {
@ -306,7 +309,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
isEnabledAccessControl,
} = getConfig();
// when using baidu api in app, not set auth header
if (isBaidu && clientConfig?.isApp) return headers;
if (isBaidu && clientConfig?.isApp)
{ return headers; }
const authHeader = getAuthHeader();
@ -318,7 +322,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
if (bearerToken) {
headers[authHeader] = bearerToken;
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
headers["Authorization"] = getBearerToken(
headers.Authorization = getBearerToken(
ACCESS_CODE_PREFIX + accessStore.accessCode,
);
}

View File

@ -19,7 +19,7 @@ export const ChatControllerPool = {
},
stopAll() {
Object.values(this.controllers).forEach((v) => v.abort());
Object.values(this.controllers).forEach(v => v.abort());
},
hasPending() {

View File

@ -1,29 +1,31 @@
"use client";
import {
ApiPath,
Alibaba,
ALIBABA_BASE_URL,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
'use client';
import type {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
MultimodalContent,
} from "../api";
import Locale from "../../locales";
SpeechOptions,
} from '../api';
import { getClientConfig } from '@/app/config/client';
import {
Alibaba,
ALIBABA_BASE_URL,
ApiPath,
REQUEST_TIMEOUT_MS,
} from '@/app/constant';
import { useAccessStore, useAppConfig, useChatStore } from '@/app/store';
import { getMessageTextContent } from '@/app/utils';
import { prettyObject } from '@/app/utils/format';
import { fetch } from '@/app/utils/stream';
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
} from '@fortaine/fetch-event-source';
import Locale from '../../locales';
import {
getHeaders,
} from '../api';
export interface OpenAIListModelResponse {
object: string;
@ -36,7 +38,7 @@ export interface OpenAIListModelResponse {
interface RequestInput {
messages: {
role: "system" | "user" | "assistant";
role: 'system' | 'user' | 'assistant';
content: string | MultimodalContent[];
}[];
}
@ -58,7 +60,7 @@ export class QwenApi implements LLMApi {
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
let baseUrl = '';
if (accessStore.useCustomConfig) {
baseUrl = accessStore.alibabaUrl;
@ -69,28 +71,28 @@ export class QwenApi implements LLMApi {
baseUrl = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Alibaba)) {
baseUrl = "https://" + baseUrl;
if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Alibaba)) {
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
console.log('[Proxy Endpoint] ', baseUrl, path);
return [baseUrl, path].join("/");
return [baseUrl, path].join('/');
}
extractMessage(res: any) {
return res?.output?.choices?.at(0)?.message?.content ?? "";
return res?.output?.choices?.at(0)?.message?.content ?? '';
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({
const messages = options.messages.map(v => ({
role: v.role,
content: getMessageTextContent(v),
}));
@ -110,7 +112,7 @@ export class QwenApi implements LLMApi {
messages,
},
parameters: {
result_format: "message",
result_format: 'message',
incremental_output: shouldStream,
temperature: modelConfig.temperature,
// max_tokens: modelConfig.max_tokens,
@ -124,12 +126,12 @@ export class QwenApi implements LLMApi {
try {
const chatPath = this.path(Alibaba.ChatPath);
const chatPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: {
...getHeaders(),
"X-DashScope-SSE": shouldStream ? "enable" : "disable",
'X-DashScope-SSE': shouldStream ? 'enable' : 'disable',
},
};
@ -140,8 +142,8 @@ export class QwenApi implements LLMApi {
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let responseText = '';
let remainText = '';
let finished = false;
let responseRes: Response;
@ -149,9 +151,9 @@ export class QwenApi implements LLMApi {
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
console.log('[Response Animation] finished');
if (responseText?.length === 0) {
options.onError?.(new Error("empty response from server"));
options.onError?.(new Error('empty response from server'));
}
return;
}
@ -184,24 +186,24 @@ export class QwenApi implements LLMApi {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
const contentType = res.headers.get('content-type');
console.log(
"[Alibaba] request response content type: ",
'[Alibaba] request response content type: ',
contentType,
);
responseRes = res;
if (contentType?.startsWith("text/plain")) {
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
!res.ok
|| !res.headers
.get('content-type')
?.startsWith(EventStreamContentType)
|| res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
@ -218,13 +220,13 @@ export class QwenApi implements LLMApi {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
responseText = responseTexts.join('\n\n');
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
if (msg.data === '[DONE]' || finished) {
return finish();
}
const text = msg.data;
@ -238,7 +240,7 @@ export class QwenApi implements LLMApi {
remainText += delta;
}
} catch (e) {
console.error("[Request] parse error", text, msg);
console.error('[Request] parse error', text, msg);
}
},
onclose() {
@ -259,10 +261,11 @@ export class QwenApi implements LLMApi {
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
console.log('[Request] failed to make a chat request', e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,

View File

@ -1,34 +1,36 @@
import { Anthropic, ApiPath } from "@/app/constant";
import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api";
import type {
ChatMessageTool,
} from '@/app/store';
import type { ChatOptions, LLMApi, SpeechOptions } from '../api';
import type { RequestPayload } from './openai';
import { getClientConfig } from '@/app/config/client';
import { Anthropic, ANTHROPIC_BASE_URL, ApiPath } from '@/app/constant';
import {
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
ChatMessageTool,
} from "@/app/store";
import { getClientConfig } from "@/app/config/client";
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";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
} from '@/app/store';
import { getMessageTextContent, isVisionModel } from '@/app/utils';
import { preProcessImageContent, stream } from '@/app/utils/chat';
import { cloudflareAIGatewayUrl } from '@/app/utils/cloudflare';
import { fetch } from '@/app/utils/stream';
import { getHeaders } from '../api';
export type MultiBlockContent = {
type: "image" | "text";
export interface MultiBlockContent {
type: 'image' | 'text';
source?: {
type: string;
media_type: string;
data: string;
};
text?: string;
};
}
export type AnthropicMessage = {
export interface AnthropicMessage {
role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper];
content: string | MultiBlockContent[];
};
}
export interface AnthropicChatRequest {
model: string; // The model that will complete your prompt.
@ -56,7 +58,7 @@ export interface ChatRequest {
export interface ChatResponse {
completion: string;
stop_reason: "stop_sequence" | "max_tokens";
stop_reason: 'stop_sequence' | 'max_tokens';
model: string;
}
@ -66,23 +68,24 @@ export type ChatStreamResponse = ChatResponse & {
};
const ClaudeMapper = {
assistant: "assistant",
user: "user",
system: "user",
assistant: 'assistant',
user: 'user',
system: 'user',
} as const;
const keys = ["claude-2, claude-instant-1"];
const keys = ['claude-2, claude-instant-1'];
export class ClaudeApi implements LLMApi {
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
extractMessage(res: any) {
console.log("[Response] claude response: ", res);
console.log('[Response] claude response: ', res);
return res?.content?.[0]?.text;
}
async chat(options: ChatOptions): Promise<void> {
const visionModel = isVisionModel(options.config.model);
@ -99,13 +102,13 @@ export class ClaudeApi implements LLMApi {
};
// try get base64image from local cache image_url
const messages: ChatOptions["messages"] = [];
const messages: ChatOptions['messages'] = [];
for (const v of options.messages) {
const content = await preProcessImageContent(v.content);
messages.push({ role: v.role, content });
}
const keys = ["system", "user"];
const keys = ['system', 'user'];
// roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
for (let i = 0; i < messages.length - 1; i++) {
@ -116,8 +119,8 @@ export class ClaudeApi implements LLMApi {
messages[i] = [
message,
{
role: "assistant",
content: ";",
role: 'assistant',
content: ';',
},
] as any;
}
@ -126,15 +129,17 @@ export class ClaudeApi implements LLMApi {
const prompt = messages
.flat()
.filter((v) => {
if (!v.content) return false;
if (typeof v.content === "string" && !v.content.trim()) return false;
if (!v.content)
{ return false; }
if (typeof v.content === 'string' && !v.content.trim())
{ return false; }
return true;
})
.map((v) => {
const { role, content } = v;
const insideRole = ClaudeMapper[role] ?? "user";
const insideRole = ClaudeMapper[role] ?? 'user';
if (!visionModel || typeof content === "string") {
if (!visionModel || typeof content === 'string') {
return {
role: insideRole,
content: getMessageTextContent(v),
@ -143,25 +148,25 @@ export class ClaudeApi implements LLMApi {
return {
role: insideRole,
content: content
.filter((v) => v.image_url || v.text)
.filter(v => v.image_url || v.text)
.map(({ type, text, image_url }) => {
if (type === "text") {
if (type === 'text') {
return {
type,
text: text!,
};
}
const { url = "" } = image_url || {};
const colonIndex = url.indexOf(":");
const semicolonIndex = url.indexOf(";");
const comma = url.indexOf(",");
const { url = '' } = image_url || {};
const colonIndex = url.indexOf(':');
const semicolonIndex = url.indexOf(';');
const comma = url.indexOf(',');
const mimeType = url.slice(colonIndex + 1, semicolonIndex);
const encodeType = url.slice(semicolonIndex + 1, comma);
const data = url.slice(comma + 1);
return {
type: "image" as const,
type: 'image' as const,
source: {
type: encodeType,
media_type: mimeType,
@ -172,10 +177,10 @@ export class ClaudeApi implements LLMApi {
};
});
if (prompt[0]?.role === "assistant") {
if (prompt[0]?.role === 'assistant') {
prompt.unshift({
role: "user",
content: ";",
role: 'user',
content: ';',
});
}
@ -208,10 +213,10 @@ export class ClaudeApi implements LLMApi {
requestBody,
{
...getHeaders(),
"anthropic-version": accessStore.anthropicApiVersion,
'anthropic-version': accessStore.anthropicApiVersion,
},
// @ts-ignore
tools.map((tool) => ({
tools.map(tool => ({
name: tool?.function?.name,
description: tool?.function?.description,
input_schema: tool?.function?.parameters,
@ -224,41 +229,41 @@ export class ClaudeApi implements LLMApi {
let chunkJson:
| undefined
| {
type: "content_block_delta" | "content_block_stop";
content_block?: {
type: "tool_use";
id: string;
name: string;
};
delta?: {
type: "text_delta" | "input_json_delta";
text?: string;
partial_json?: string;
};
index: number;
type: 'content_block_delta' | 'content_block_stop';
content_block?: {
type: 'tool_use';
id: string;
name: string;
};
delta?: {
type: 'text_delta' | 'input_json_delta';
text?: string;
partial_json?: string;
};
index: number;
};
chunkJson = JSON.parse(text);
if (chunkJson?.content_block?.type == "tool_use") {
if (chunkJson?.content_block?.type == 'tool_use') {
index += 1;
const id = chunkJson?.content_block.id;
const name = chunkJson?.content_block.name;
runTools.push({
id,
type: "function",
type: 'function',
function: {
name,
arguments: "",
arguments: '',
},
});
}
if (
chunkJson?.delta?.type == "input_json_delta" &&
chunkJson?.delta?.partial_json
chunkJson?.delta?.type == 'input_json_delta'
&& chunkJson?.delta?.partial_json
) {
// @ts-ignore
runTools[index]["function"]["arguments"] +=
chunkJson?.delta?.partial_json;
runTools[index].function.arguments
+= chunkJson?.delta?.partial_json;
}
return chunkJson?.delta?.text;
},
@ -276,10 +281,10 @@ export class ClaudeApi implements LLMApi {
requestPayload?.messages?.length,
0,
{
role: "assistant",
role: 'assistant',
content: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
type: "tool_use",
type: 'tool_use',
id: tool.id,
name: tool?.function?.name,
input: tool?.function?.arguments
@ -289,11 +294,11 @@ export class ClaudeApi implements LLMApi {
),
},
// @ts-ignore
...toolCallResult.map((result) => ({
role: "user",
...toolCallResult.map(result => ({
role: 'user',
content: [
{
type: "tool_result",
type: 'tool_result',
tool_use_id: result.tool_call_id,
content: result.content,
},
@ -305,12 +310,12 @@ export class ClaudeApi implements LLMApi {
);
} else {
const payload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestBody),
signal: controller.signal,
headers: {
...getHeaders(), // get common headers
"anthropic-version": accessStore.anthropicApiVersion,
'anthropic-version': accessStore.anthropicApiVersion,
// do not send `anthropicApiKey` in browser!!!
// Authorization: getAuthKey(accessStore.anthropicApiKey),
},
@ -318,7 +323,7 @@ export class ClaudeApi implements LLMApi {
try {
controller.signal.onabort = () =>
options.onFinish("", new Response(null, { status: 400 }));
options.onFinish('', new Response(null, { status: 400 }));
const res = await fetch(path, payload);
const resJson = await res.json();
@ -326,17 +331,19 @@ export class ClaudeApi implements LLMApi {
const message = this.extractMessage(resJson);
options.onFinish(message, res);
} catch (e) {
console.error("failed to chat", e);
console.error('failed to chat', e);
options.onError?.(e as Error);
}
}
}
async usage() {
return {
used: 0,
total: 0,
};
}
async models() {
// const provider = {
// id: "anthropic",
@ -377,10 +384,11 @@ export class ClaudeApi implements LLMApi {
// },
];
}
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl: string = "";
let baseUrl: string = '';
if (accessStore.useCustomConfig) {
baseUrl = accessStore.anthropicUrl;
@ -393,19 +401,20 @@ export class ClaudeApi implements LLMApi {
baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
baseUrl = "https://" + baseUrl;
if (!baseUrl.startsWith('http') && !baseUrl.startsWith('/api')) {
baseUrl = `https://${baseUrl}`;
}
baseUrl = trimEnd(baseUrl, "/");
baseUrl = trimEnd(baseUrl, '/');
// try rebuild url, when using cloudflare ai gateway in client
return cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
}
}
function trimEnd(s: string, end = " ") {
if (end.length === 0) return s;
function trimEnd(s: string, end = ' ') {
if (end.length === 0)
{ return s; }
while (s.endsWith(end)) {
s = s.slice(0, -end.length);

View File

@ -1,30 +1,32 @@
"use client";
'use client';
import type {
ChatOptions,
LLMApi,
LLMModel,
MultimodalContent,
SpeechOptions,
} from '../api';
import { getClientConfig } from '@/app/config/client';
import {
ApiPath,
Baidu,
BAIDU_BASE_URL,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { getAccessToken } from "@/app/utils/baidu";
} from '@/app/constant';
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import { useAccessStore, useAppConfig, useChatStore } from '@/app/store';
import { getMessageTextContent } from '@/app/utils';
import { getAccessToken } from '@/app/utils/baidu';
import { prettyObject } from '@/app/utils/format';
import { fetch } from '@/app/utils/stream';
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
} from '@fortaine/fetch-event-source';
import Locale from '../../locales';
import {
getHeaders,
} from '../api';
export interface OpenAIListModelResponse {
object: string;
@ -37,7 +39,7 @@ export interface OpenAIListModelResponse {
interface RequestPayload {
messages: {
role: "system" | "user" | "assistant";
role: 'system' | 'user' | 'assistant';
content: string | MultimodalContent[];
}[];
stream?: boolean;
@ -53,7 +55,7 @@ export class ErnieApi implements LLMApi {
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
let baseUrl = '';
if (accessStore.useCustomConfig) {
baseUrl = accessStore.baiduUrl;
@ -65,40 +67,40 @@ export class ErnieApi implements LLMApi {
baseUrl = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Baidu)) {
baseUrl = "https://" + baseUrl;
if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Baidu)) {
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
console.log('[Proxy Endpoint] ', baseUrl, path);
return [baseUrl, path].join("/");
return [baseUrl, path].join('/');
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({
const messages = options.messages.map(v => ({
// "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
role: v.role === "system" ? "user" : v.role,
role: v.role === 'system' ? 'user' : v.role,
content: getMessageTextContent(v),
}));
// "error_code": 336006, "error_msg": "the length of messages must be an odd number",
if (messages.length % 2 === 0) {
if (messages.at(0)?.role === "user") {
if (messages.at(0)?.role === 'user') {
messages.splice(1, 0, {
role: "assistant",
content: " ",
role: 'assistant',
content: ' ',
});
} else {
messages.unshift({
role: "user",
content: " ",
role: 'user',
content: ' ',
});
}
}
@ -122,7 +124,7 @@ export class ErnieApi implements LLMApi {
top_p: modelConfig.top_p,
};
console.log("[Request] Baidu payload: ", requestPayload);
console.log('[Request] Baidu payload: ', requestPayload);
const controller = new AbortController();
options.onController?.(controller);
@ -131,7 +133,7 @@ export class ErnieApi implements LLMApi {
let chatPath = this.path(Baidu.ChatPath(modelConfig.model));
// getAccessToken can not run in browser, because cors error
if (!!getClientConfig()?.isApp) {
if (getClientConfig()?.isApp) {
const accessStore = useAccessStore.getState();
if (accessStore.useCustomConfig) {
if (accessStore.isValidBaidu()) {
@ -140,13 +142,13 @@ export class ErnieApi implements LLMApi {
accessStore.baiduSecretKey,
);
chatPath = `${chatPath}${
chatPath.includes("?") ? "&" : "?"
chatPath.includes('?') ? '&' : '?'
}access_token=${access_token}`;
}
}
}
const chatPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
@ -159,8 +161,8 @@ export class ErnieApi implements LLMApi {
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let responseText = '';
let remainText = '';
let finished = false;
let responseRes: Response;
@ -168,9 +170,9 @@ export class ErnieApi implements LLMApi {
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
console.log('[Response Animation] finished');
if (responseText?.length === 0) {
options.onError?.(new Error("empty response from server"));
options.onError?.(new Error('empty response from server'));
}
return;
}
@ -203,20 +205,20 @@ export class ErnieApi implements LLMApi {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log("[Baidu] request response content type: ", contentType);
const contentType = res.headers.get('content-type');
console.log('[Baidu] request response content type: ', contentType);
responseRes = res;
if (contentType?.startsWith("text/plain")) {
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
!res.ok
|| !res.headers
.get('content-type')
?.startsWith(EventStreamContentType)
|| res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
@ -233,13 +235,13 @@ export class ErnieApi implements LLMApi {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
responseText = responseTexts.join('\n\n');
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
if (msg.data === '[DONE]' || finished) {
return finish();
}
const text = msg.data;
@ -250,7 +252,7 @@ export class ErnieApi implements LLMApi {
remainText += delta;
}
} catch (e) {
console.error("[Request] parse error", text, msg);
console.error('[Request] parse error', text, msg);
}
},
onclose() {
@ -271,10 +273,11 @@ export class ErnieApi implements LLMApi {
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
console.log('[Request] failed to make a chat request', e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,

View File

@ -1,29 +1,31 @@
"use client";
'use client';
import type {
ChatOptions,
LLMApi,
LLMModel,
MultimodalContent,
SpeechOptions,
} from '../api';
import { getClientConfig } from '@/app/config/client';
import {
ApiPath,
ByteDance,
BYTEDANCE_BASE_URL,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
} from '@/app/constant';
import { useAccessStore, useAppConfig, useChatStore } from '@/app/store';
import { getMessageTextContent } from '@/app/utils';
import { prettyObject } from '@/app/utils/format';
import { fetch } from '@/app/utils/stream';
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
} from '@fortaine/fetch-event-source';
import Locale from '../../locales';
import {
getHeaders,
} from '../api';
export interface OpenAIListModelResponse {
object: string;
@ -36,7 +38,7 @@ export interface OpenAIListModelResponse {
interface RequestPayload {
messages: {
role: "system" | "user" | "assistant";
role: 'system' | 'user' | 'assistant';
content: string | MultimodalContent[];
}[];
stream?: boolean;
@ -52,7 +54,7 @@ export class DoubaoApi implements LLMApi {
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
let baseUrl = '';
if (accessStore.useCustomConfig) {
baseUrl = accessStore.bytedanceUrl;
@ -63,28 +65,28 @@ export class DoubaoApi implements LLMApi {
baseUrl = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ByteDance)) {
baseUrl = "https://" + baseUrl;
if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.ByteDance)) {
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
console.log('[Proxy Endpoint] ', baseUrl, path);
return [baseUrl, path].join("/");
return [baseUrl, path].join('/');
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
return res.choices?.at(0)?.message?.content ?? '';
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({
const messages = options.messages.map(v => ({
role: v.role,
content: getMessageTextContent(v),
}));
@ -114,7 +116,7 @@ export class DoubaoApi implements LLMApi {
try {
const chatPath = this.path(ByteDance.ChatPath);
const chatPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
@ -127,8 +129,8 @@ export class DoubaoApi implements LLMApi {
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let responseText = '';
let remainText = '';
let finished = false;
let responseRes: Response;
@ -136,9 +138,9 @@ export class DoubaoApi implements LLMApi {
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
console.log('[Response Animation] finished');
if (responseText?.length === 0) {
options.onError?.(new Error("empty response from server"));
options.onError?.(new Error('empty response from server'));
}
return;
}
@ -171,23 +173,23 @@ export class DoubaoApi implements LLMApi {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
const contentType = res.headers.get('content-type');
console.log(
"[ByteDance] request response content type: ",
'[ByteDance] request response content type: ',
contentType,
);
responseRes = res;
if (contentType?.startsWith("text/plain")) {
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
!res.ok
|| !res.headers
.get('content-type')
?.startsWith(EventStreamContentType)
|| res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
@ -204,13 +206,13 @@ export class DoubaoApi implements LLMApi {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
responseText = responseTexts.join('\n\n');
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
if (msg.data === '[DONE]' || finished) {
return finish();
}
const text = msg.data;
@ -224,7 +226,7 @@ export class DoubaoApi implements LLMApi {
remainText += delta;
}
} catch (e) {
console.error("[Request] parse error", text, msg);
console.error('[Request] parse error', text, msg);
}
},
onclose() {
@ -245,10 +247,11 @@ export class DoubaoApi implements LLMApi {
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
console.log('[Request] failed to make a chat request', e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,

View File

@ -1,29 +1,33 @@
"use client";
'use client';
import type {
ChatMessageTool,
} from '@/app/store';
import type {
ChatOptions,
LLMApi,
LLMModel,
SpeechOptions,
} from '../api';
import type { RequestPayload } from './openai';
import { getClientConfig } from '@/app/config/client';
import {
ApiPath,
CHATGLM_BASE_URL,
ChatGLM,
CHATGLM_BASE_URL,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
} from '@/app/constant';
import {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
} from '@/app/store';
import { getMessageTextContent } from '@/app/utils';
import { stream } from '@/app/utils/chat';
import { fetch } from '@/app/utils/stream';
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
} from '../api';
export class ChatGLMApi implements LLMApi {
private disableListModels = true;
@ -31,7 +35,7 @@ export class ChatGLMApi implements LLMApi {
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
let baseUrl = '';
if (accessStore.useCustomConfig) {
baseUrl = accessStore.chatglmUrl;
@ -43,28 +47,28 @@ export class ChatGLMApi implements LLMApi {
baseUrl = isApp ? CHATGLM_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) {
baseUrl = "https://" + baseUrl;
if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.ChatGLM)) {
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
console.log('[Proxy Endpoint] ', baseUrl, path);
return [baseUrl, path].join("/");
return [baseUrl, path].join('/');
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
return res.choices?.at(0)?.message?.content ?? '';
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
const messages: ChatOptions['messages'] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
messages.push({ role: v.role, content });
@ -89,7 +93,7 @@ export class ChatGLMApi implements LLMApi {
top_p: modelConfig.top_p,
};
console.log("[Request] glm payload: ", requestPayload);
console.log('[Request] glm payload: ', requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
@ -98,7 +102,7 @@ export class ChatGLMApi implements LLMApi {
try {
const chatPath = this.path(ChatGLM.ChatPath);
const chatPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
@ -149,7 +153,7 @@ export class ChatGLMApi implements LLMApi {
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
runTools[index].function.arguments += args;
}
}
return choices[0]?.delta?.content;
@ -180,10 +184,11 @@ export class ChatGLMApi implements LLMApi {
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
console.log('[Request] failed to make a chat request', e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,

View File

@ -1,38 +1,40 @@
import { ApiPath, Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
import {
import type {
ChatMessageTool,
} from '@/app/store';
import type {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
LLMUsage,
SpeechOptions,
} from "../api";
} from '../api';
import type { RequestPayload } from './openai';
import { getClientConfig } from '@/app/config/client';
import { ApiPath, GEMINI_BASE_URL, Google, REQUEST_TIMEOUT_MS } from '@/app/constant';
import {
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
ChatMessageTool,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
import { getClientConfig } from "@/app/config/client";
import { GEMINI_BASE_URL } from "@/app/constant";
} from '@/app/store';
import {
getMessageTextContent,
getMessageImages,
getMessageTextContent,
isVisionModel,
} from "@/app/utils";
import { preProcessImageContent } from "@/app/utils/chat";
import { nanoid } from "nanoid";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
} from '@/app/utils';
import { preProcessImageContent, stream } from '@/app/utils/chat';
import { fetch } from '@/app/utils/stream';
import { nanoid } from 'nanoid';
import {
getHeaders,
} from '../api';
export class GeminiProApi implements LLMApi {
path(path: string, shouldStream = false): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
let baseUrl = '';
if (accessStore.useCustomConfig) {
baseUrl = accessStore.googleUrl;
}
@ -41,34 +43,36 @@ export class GeminiProApi implements LLMApi {
if (baseUrl.length === 0) {
baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Google)) {
baseUrl = "https://" + baseUrl;
if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Google)) {
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
console.log('[Proxy Endpoint] ', baseUrl, path);
let chatPath = [baseUrl, path].join("/");
let chatPath = [baseUrl, path].join('/');
if (shouldStream) {
chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
chatPath += chatPath.includes('?') ? '&alt=sse' : '?alt=sse';
}
return chatPath;
}
extractMessage(res: any) {
console.log("[Response] gemini-pro response: ", res);
console.log('[Response] gemini-pro response: ', res);
return (
res?.candidates?.at(0)?.content?.parts.at(0)?.text ||
res?.at(0)?.candidates?.at(0)?.content?.parts.at(0)?.text ||
res?.error?.message ||
""
res?.candidates?.at(0)?.content?.parts.at(0)?.text
|| res?.at(0)?.candidates?.at(0)?.content?.parts.at(0)?.text
|| res?.error?.message
|| ''
);
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
async chat(options: ChatOptions): Promise<void> {
@ -76,7 +80,7 @@ export class GeminiProApi implements LLMApi {
let multimodal = false;
// try get base64image from local cache image_url
const _messages: ChatOptions["messages"] = [];
const _messages: ChatOptions['messages'] = [];
for (const v of options.messages) {
const content = await preProcessImageContent(v.content);
_messages.push({ role: v.role, content });
@ -89,8 +93,8 @@ export class GeminiProApi implements LLMApi {
multimodal = true;
parts = parts.concat(
images.map((image) => {
const imageType = image.split(";")[0].split(":")[1];
const imageData = image.split(",")[1];
const imageType = image.split(';')[0].split(':')[1];
const imageData = image.split(',')[1];
return {
inline_data: {
mime_type: imageType,
@ -102,13 +106,13 @@ export class GeminiProApi implements LLMApi {
}
}
return {
role: v.role.replace("assistant", "model").replace("system", "user"),
parts: parts,
role: v.role.replace('assistant', 'model').replace('system', 'user'),
parts,
};
});
// google requires that role in neighboring messages must not be the same
for (let i = 0; i < messages.length - 1; ) {
for (let i = 0; i < messages.length - 1;) {
// Check if current and next item both have the role "model"
if (messages[i].role === messages[i + 1].role) {
// Concatenate the 'parts' of the current and next item
@ -146,25 +150,25 @@ export class GeminiProApi implements LLMApi {
},
safetySettings: [
{
category: "HARM_CATEGORY_HARASSMENT",
category: 'HARM_CATEGORY_HARASSMENT',
threshold: accessStore.googleSafetySettings,
},
{
category: "HARM_CATEGORY_HATE_SPEECH",
category: 'HARM_CATEGORY_HATE_SPEECH',
threshold: accessStore.googleSafetySettings,
},
{
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
threshold: accessStore.googleSafetySettings,
},
{
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
threshold: accessStore.googleSafetySettings,
},
],
};
let shouldStream = !!options.config.stream;
const shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
@ -175,7 +179,7 @@ export class GeminiProApi implements LLMApi {
);
const chatPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
@ -200,7 +204,7 @@ export class GeminiProApi implements LLMApi {
// @ts-ignore
tools.length > 0
? // @ts-ignore
[{ functionDeclarations: tools.map((tool) => tool.function) }]
[{ functionDeclarations: tools.map(tool => tool.function) }]
: [],
funcs,
controller,
@ -211,12 +215,15 @@ export class GeminiProApi implements LLMApi {
const functionCall = chunkJson?.candidates
?.at(0)
?.content.parts.at(0)?.functionCall;
?.content
.parts
.at(0)
?.functionCall;
if (functionCall) {
const { name, args } = functionCall;
runTools.push({
id: nanoid(),
type: "function",
type: 'function',
function: {
name,
arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
@ -237,7 +244,7 @@ export class GeminiProApi implements LLMApi {
requestPayload?.contents?.length,
0,
{
role: "model",
role: 'model',
parts: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
functionCall: {
@ -248,8 +255,8 @@ export class GeminiProApi implements LLMApi {
),
},
// @ts-ignore
...toolCallResult.map((result) => ({
role: "function",
...toolCallResult.map(result => ({
role: 'function',
parts: [
{
functionResponse: {
@ -274,8 +281,8 @@ export class GeminiProApi implements LLMApi {
// being blocked
options.onError?.(
new Error(
"Message is being blocked for reason: " +
resJson.promptFeedback.blockReason,
`Message is being blocked for reason: ${
resJson.promptFeedback.blockReason}`,
),
);
}
@ -283,13 +290,15 @@ export class GeminiProApi implements LLMApi {
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
console.log('[Request] failed to make a chat request', e);
options.onError?.(e as Error);
}
}
usage(): Promise<LLMUsage> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
async models(): Promise<LLMModel[]> {
return [];
}

View File

@ -1,30 +1,32 @@
"use client";
import {
ApiPath,
IFLYTEK_BASE_URL,
Iflytek,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
'use client';
import type {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
} from '../api';
import type { RequestPayload } from './openai';
import { getClientConfig } from '@/app/config/client';
import {
ApiPath,
Iflytek,
IFLYTEK_BASE_URL,
REQUEST_TIMEOUT_MS,
} from '@/app/constant';
import { useAccessStore, useAppConfig, useChatStore } from '@/app/store';
import { getMessageTextContent } from '@/app/utils';
import { prettyObject } from '@/app/utils/format';
import { fetch } from '@/app/utils/stream';
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
} from '@fortaine/fetch-event-source';
import Locale from '../../locales';
import { RequestPayload } from "./openai";
import {
getHeaders,
} from '../api';
export class SparkApi implements LLMApi {
private disableListModels = true;
@ -32,7 +34,7 @@ export class SparkApi implements LLMApi {
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
let baseUrl = '';
if (accessStore.useCustomConfig) {
baseUrl = accessStore.iflytekUrl;
@ -44,28 +46,28 @@ export class SparkApi implements LLMApi {
baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) {
baseUrl = "https://" + baseUrl;
if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Iflytek)) {
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
console.log('[Proxy Endpoint] ', baseUrl, path);
return [baseUrl, path].join("/");
return [baseUrl, path].join('/');
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
return res.choices?.at(0)?.message?.content ?? '';
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
const messages: ChatOptions['messages'] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
messages.push({ role: v.role, content });
@ -92,7 +94,7 @@ export class SparkApi implements LLMApi {
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
};
console.log("[Request] Spark payload: ", requestPayload);
console.log('[Request] Spark payload: ', requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
@ -101,7 +103,7 @@ export class SparkApi implements LLMApi {
try {
const chatPath = this.path(Iflytek.ChatPath);
const chatPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
@ -114,8 +116,8 @@ export class SparkApi implements LLMApi {
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let responseText = '';
let remainText = '';
let finished = false;
let responseRes: Response;
@ -123,7 +125,7 @@ export class SparkApi implements LLMApi {
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
console.log('[Response Animation] finished');
return;
}
@ -155,21 +157,21 @@ export class SparkApi implements LLMApi {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log("[Spark] request response content type: ", contentType);
const contentType = res.headers.get('content-type');
console.log('[Spark] request response content type: ', contentType);
responseRes = res;
if (contentType?.startsWith("text/plain")) {
if (contentType?.startsWith('text/plain')) {
responseText = await res.clone().text();
return finish();
}
// Handle different error scenarios
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
!res.ok
|| !res.headers
.get('content-type')
?.startsWith(EventStreamContentType)
|| res.status !== 200
) {
let extraInfo = await res.clone().text();
try {
@ -190,7 +192,7 @@ export class SparkApi implements LLMApi {
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
if (msg.data === '[DONE]' || finished) {
return finish();
}
const text = msg.data;
@ -205,7 +207,7 @@ export class SparkApi implements LLMApi {
remainText += delta;
}
} catch (e) {
console.error("[Request] parse error", text);
console.error('[Request] parse error', text);
options.onError?.(new Error(`Failed to parse response: ${text}`));
}
},
@ -235,7 +237,7 @@ export class SparkApi implements LLMApi {
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
console.log('[Request] failed to make a chat request', e);
options.onError?.(e as Error);
}
}

View File

@ -1,30 +1,34 @@
"use client";
'use client';
import type {
ChatMessageTool,
} from '@/app/store';
import type {
ChatOptions,
LLMApi,
LLMModel,
SpeechOptions,
} from '../api';
import type { RequestPayload } from './openai';
import { getClientConfig } from '@/app/config/client';
// azure and openai, using same models. so using same LLMApi.
import {
ApiPath,
MOONSHOT_BASE_URL,
Moonshot,
MOONSHOT_BASE_URL,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
} from '@/app/constant';
import {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
} from '@/app/store';
import { getMessageTextContent } from '@/app/utils';
import { stream } from '@/app/utils/chat';
import { fetch } from '@/app/utils/stream';
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
} from '../api';
export class MoonshotApi implements LLMApi {
private disableListModels = true;
@ -32,7 +36,7 @@ export class MoonshotApi implements LLMApi {
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
let baseUrl = '';
if (accessStore.useCustomConfig) {
baseUrl = accessStore.moonshotUrl;
@ -44,28 +48,28 @@ export class MoonshotApi implements LLMApi {
baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Moonshot)) {
baseUrl = "https://" + baseUrl;
if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Moonshot)) {
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
console.log('[Proxy Endpoint] ', baseUrl, path);
return [baseUrl, path].join("/");
return [baseUrl, path].join('/');
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
return res.choices?.at(0)?.message?.content ?? '';
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
const messages: ChatOptions['messages'] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
messages.push({ role: v.role, content });
@ -92,7 +96,7 @@ export class MoonshotApi implements LLMApi {
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
};
console.log("[Request] openai payload: ", requestPayload);
console.log('[Request] openai payload: ', requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
@ -101,7 +105,7 @@ export class MoonshotApi implements LLMApi {
try {
const chatPath = this.path(Moonshot.ChatPath);
const chatPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
@ -152,7 +156,7 @@ export class MoonshotApi implements LLMApi {
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
runTools[index].function.arguments += args;
}
}
return choices[0]?.delta?.content;
@ -183,10 +187,11 @@ export class MoonshotApi implements LLMApi {
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
console.log('[Request] failed to make a chat request', e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,

View File

@ -1,48 +1,52 @@
"use client";
// azure and openai, using same models. so using same LLMApi.
import {
ApiPath,
OPENAI_BASE_URL,
DEFAULT_MODELS,
OpenaiPath,
Azure,
REQUEST_TIMEOUT_MS,
ServiceProvider,
} from "@/app/constant";
import {
'use client';
import type {
ChatMessageTool,
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
} from "@/app/store";
import { collectModelsWithDefaultModel } from "@/app/utils/model";
import {
preProcessImageContent,
uploadImage,
base64Image2Blob,
stream,
} from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
import {
} from '@/app/store';
import type { DalleQuality, DalleSize, DalleStyle } from '@/app/typing';
import type {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
LLMUsage,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import { getClientConfig } from "@/app/config/client";
} from '../api';
import { getClientConfig } from '@/app/config/client';
// azure and openai, using same models. so using same LLMApi.
import {
ApiPath,
Azure,
DEFAULT_MODELS,
OPENAI_BASE_URL,
OpenaiPath,
REQUEST_TIMEOUT_MS,
ServiceProvider,
} from '@/app/constant';
import {
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
} from '@/app/store';
import {
isDalle3 as _isDalle3,
getMessageTextContent,
isVisionModel,
isDalle3 as _isDalle3,
} from "@/app/utils";
import { fetch } from "@/app/utils/stream";
} from '@/app/utils';
import {
base64Image2Blob,
preProcessImageContent,
stream,
uploadImage,
} from '@/app/utils/chat';
import { cloudflareAIGatewayUrl } from '@/app/utils/cloudflare';
import { collectModelsWithDefaultModel } from '@/app/utils/model';
import { fetch } from '@/app/utils/stream';
import Locale from '../../locales';
import {
getHeaders,
} from '../api';
export interface OpenAIListModelResponse {
object: string;
@ -55,7 +59,7 @@ export interface OpenAIListModelResponse {
export interface RequestPayload {
messages: {
role: "system" | "user" | "assistant";
role: 'system' | 'user' | 'assistant';
content: string | MultimodalContent[];
}[];
stream?: boolean;
@ -71,7 +75,7 @@ export interface RequestPayload {
export interface DalleRequestPayload {
model: string;
prompt: string;
response_format: "url" | "b64_json";
response_format: 'url' | 'b64_json';
n: number;
size: DalleSize;
quality: DalleQuality;
@ -84,13 +88,13 @@ export class ChatGPTApi implements LLMApi {
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
let baseUrl = '';
const isAzure = path.includes("deployments");
const isAzure = path.includes('deployments');
if (accessStore.useCustomConfig) {
if (isAzure && !accessStore.isValidAzure()) {
throw Error(
"incomplete azure config, please check it in your settings page",
throw new Error(
'incomplete azure config, please check it in your settings page',
);
}
@ -103,38 +107,38 @@ export class ChatGPTApi implements LLMApi {
baseUrl = isApp ? OPENAI_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (
!baseUrl.startsWith("http") &&
!isAzure &&
!baseUrl.startsWith(ApiPath.OpenAI)
!baseUrl.startsWith('http')
&& !isAzure
&& !baseUrl.startsWith(ApiPath.OpenAI)
) {
baseUrl = "https://" + baseUrl;
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
console.log('[Proxy Endpoint] ', baseUrl, path);
// try rebuild url, when using cloudflare ai gateway in client
return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
return cloudflareAIGatewayUrl([baseUrl, path].join('/'));
}
async extractMessage(res: any) {
if (res.error) {
return "```\n" + JSON.stringify(res, null, 4) + "\n```";
return `\`\`\`\n${JSON.stringify(res, null, 4)}\n\`\`\``;
}
// dalle3 model return url, using url create image message
if (res.data) {
let url = res.data?.at(0)?.url ?? "";
const b64_json = res.data?.at(0)?.b64_json ?? "";
let url = res.data?.at(0)?.url ?? '';
const b64_json = res.data?.at(0)?.b64_json ?? '';
if (!url && b64_json) {
// uploadImage
url = await uploadImage(base64Image2Blob(b64_json, "image/png"));
url = await uploadImage(base64Image2Blob(b64_json, 'image/png'));
}
return [
{
type: "image_url",
type: 'image_url',
image_url: {
url,
},
@ -153,7 +157,7 @@ export class ChatGPTApi implements LLMApi {
speed: options.speed,
};
console.log("[Request] openai speech payload: ", requestPayload);
console.log('[Request] openai speech payload: ', requestPayload);
const controller = new AbortController();
options.onController?.(controller);
@ -161,7 +165,7 @@ export class ChatGPTApi implements LLMApi {
try {
const speechPath = this.path(OpenaiPath.SpeechPath);
const speechPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
@ -177,7 +181,7 @@ export class ChatGPTApi implements LLMApi {
clearTimeout(requestTimeoutId);
return await res.arrayBuffer();
} catch (e) {
console.log("[Request] failed to make a speech request", e);
console.log('[Request] failed to make a speech request', e);
throw e;
}
}
@ -195,7 +199,7 @@ export class ChatGPTApi implements LLMApi {
let requestPayload: RequestPayload | DalleRequestPayload;
const isDalle3 = _isDalle3(options.config.model);
const isO1 = options.config.model.startsWith("o1");
const isO1 = options.config.model.startsWith('o1');
if (isDalle3) {
const prompt = getMessageTextContent(
options.messages.slice(-1)?.pop() as any,
@ -204,21 +208,21 @@ export class ChatGPTApi implements LLMApi {
model: options.config.model,
prompt,
// URLs are only valid for 60 minutes after the image has been generated.
response_format: "b64_json", // using b64_json, and save image in CacheStorage
response_format: 'b64_json', // using b64_json, and save image in CacheStorage
n: 1,
size: options.config?.size ?? "1024x1024",
quality: options.config?.quality ?? "standard",
style: options.config?.style ?? "vivid",
size: options.config?.size ?? '1024x1024',
quality: options.config?.quality ?? 'standard',
style: options.config?.style ?? 'vivid',
};
} else {
const visionModel = isVisionModel(options.config.model);
const messages: ChatOptions["messages"] = [];
const messages: ChatOptions['messages'] = [];
for (const v of options.messages) {
const content = visionModel
? await preProcessImageContent(v.content)
: getMessageTextContent(v);
if (!(isO1 && v.role === "system"))
messages.push({ role: v.role, content });
if (!(isO1 && v.role === 'system'))
{ messages.push({ role: v.role, content }); }
}
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
@ -236,27 +240,27 @@ export class ChatGPTApi implements LLMApi {
// O1 使用 max_completion_tokens 控制token数 (https://platform.openai.com/docs/guides/reasoning#controlling-costs)
if (isO1) {
requestPayload["max_completion_tokens"] = modelConfig.max_tokens;
requestPayload.max_completion_tokens = modelConfig.max_tokens;
}
// add max_tokens to vision model
if (visionModel) {
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
requestPayload.max_tokens = Math.max(modelConfig.max_tokens, 4000);
}
}
console.log("[Request] openai payload: ", requestPayload);
console.log('[Request] openai payload: ', requestPayload);
const shouldStream = !isDalle3 && !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
let chatPath = "";
let chatPath = '';
if (modelConfig.providerName === ServiceProvider.Azure) {
// find model, and get displayName as deployName
const { models: configModels, customModels: configCustomModels } =
useAppConfig.getState();
const { models: configModels, customModels: configCustomModels }
= useAppConfig.getState();
const {
defaultModel,
customModels: accessCustomModels,
@ -264,18 +268,18 @@ export class ChatGPTApi implements LLMApi {
} = useAccessStore.getState();
const models = collectModelsWithDefaultModel(
configModels,
[configCustomModels, accessCustomModels].join(","),
[configCustomModels, accessCustomModels].join(','),
defaultModel,
);
const model = models.find(
(model) =>
model.name === modelConfig.model &&
model?.provider?.providerName === ServiceProvider.Azure,
model =>
model.name === modelConfig.model
&& model?.provider?.providerName === ServiceProvider.Azure,
);
chatPath = this.path(
(isDalle3 ? Azure.ImagePath : Azure.ChatPath)(
(model?.displayName ?? model?.name) as string,
useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
useCustomConfig ? useAccessStore.getState().azureApiVersion : '',
),
);
} else {
@ -324,7 +328,7 @@ export class ChatGPTApi implements LLMApi {
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
runTools[index].function.arguments += args;
}
}
return choices[0]?.delta?.content;
@ -350,7 +354,7 @@ export class ChatGPTApi implements LLMApi {
);
} else {
const chatPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
@ -370,16 +374,17 @@ export class ChatGPTApi implements LLMApi {
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
console.log('[Request] failed to make a chat request', e);
options.onError?.(e as Error);
}
}
async usage() {
const formatDate = (d: Date) =>
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d
.getDate()
.toString()
.padStart(2, "0")}`;
.padStart(2, '0')}`;
const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
@ -392,12 +397,12 @@ export class ChatGPTApi implements LLMApi {
`${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
),
{
method: "GET",
method: 'GET',
headers: getHeaders(),
},
),
fetch(this.path(OpenaiPath.SubsPath), {
method: "GET",
method: 'GET',
headers: getHeaders(),
}),
]);
@ -407,7 +412,7 @@ export class ChatGPTApi implements LLMApi {
}
if (!used.ok || !subs.ok) {
throw new Error("Failed to query usage from openai");
throw new Error('Failed to query usage from openai');
}
const response = (await used.json()) as {
@ -423,7 +428,7 @@ export class ChatGPTApi implements LLMApi {
};
if (response.error && response.error.type) {
throw Error(response.error.message);
throw new Error(response.error.message);
}
if (response.total_usage) {
@ -446,7 +451,7 @@ export class ChatGPTApi implements LLMApi {
}
const res = await fetch(this.path(OpenaiPath.ListModelPath), {
method: "GET",
method: 'GET',
headers: {
...getHeaders(),
},
@ -454,24 +459,24 @@ export class ChatGPTApi implements LLMApi {
const resJson = (await res.json()) as OpenAIListModelResponse;
const chatModels = resJson.data?.filter(
(m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
m => m.id.startsWith('gpt-') || m.id.startsWith('chatgpt-'),
);
console.log("[Models]", chatModels);
console.log('[Models]', chatModels);
if (!chatModels) {
return [];
}
//由于目前 OpenAI 的 disableListModels 默认为 true所以当前实际不会运行到这场
let seq = 1000; //同 Constant.ts 中的排序保持一致
return chatModels.map((m) => ({
// 由于目前 OpenAI 的 disableListModels 默认为 true所以当前实际不会运行到这场
let seq = 1000; // 同 Constant.ts 中的排序保持一致
return chatModels.map(m => ({
name: m.id,
available: true,
sorted: seq++,
provider: {
id: "openai",
providerName: "OpenAI",
providerType: "openai",
id: 'openai',
providerName: 'OpenAI',
providerType: 'openai',
sorted: 1,
},
}));

View File

@ -1,28 +1,30 @@
"use client";
import { ApiPath, TENCENT_BASE_URL, REQUEST_TIMEOUT_MS } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
'use client';
import type {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
} from '../api';
import { getClientConfig } from '@/app/config/client';
import { ApiPath, REQUEST_TIMEOUT_MS, TENCENT_BASE_URL } from '@/app/constant';
import { useAccessStore, useAppConfig, useChatStore } from '@/app/store';
import { getMessageTextContent, isVisionModel } from '@/app/utils';
import { prettyObject } from '@/app/utils/format';
import { fetch } from '@/app/utils/stream';
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent, isVisionModel } from "@/app/utils";
import mapKeys from "lodash-es/mapKeys";
import mapValues from "lodash-es/mapValues";
import isArray from "lodash-es/isArray";
import isObject from "lodash-es/isObject";
import { fetch } from "@/app/utils/stream";
} from '@fortaine/fetch-event-source';
import isArray from 'lodash-es/isArray';
import isObject from 'lodash-es/isObject';
import mapKeys from 'lodash-es/mapKeys';
import mapValues from 'lodash-es/mapValues';
import Locale from '../../locales';
import {
getHeaders,
} from '../api';
export interface OpenAIListModelResponse {
object: string;
@ -35,7 +37,7 @@ export interface OpenAIListModelResponse {
interface RequestPayload {
Messages: {
Role: "system" | "user" | "assistant";
Role: 'system' | 'user' | 'assistant';
Content: string | MultimodalContent[];
}[];
Stream?: boolean;
@ -50,8 +52,7 @@ function capitalizeKeys(obj: any): any {
} else if (isObject(obj)) {
return mapValues(
mapKeys(obj, (value: any, key: string) =>
key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase()),
),
key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase())),
capitalizeKeys,
);
} else {
@ -63,7 +64,7 @@ export class HunyuanApi implements LLMApi {
path(): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
let baseUrl = '';
if (accessStore.useCustomConfig) {
baseUrl = accessStore.tencentUrl;
@ -74,30 +75,30 @@ export class HunyuanApi implements LLMApi {
baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Tencent)) {
baseUrl = "https://" + baseUrl;
if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Tencent)) {
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy Endpoint] ", baseUrl);
console.log('[Proxy Endpoint] ', baseUrl);
return baseUrl;
}
extractMessage(res: any) {
return res.Choices?.at(0)?.Message?.Content ?? "";
return res.Choices?.at(0)?.Message?.Content ?? '';
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
async chat(options: ChatOptions) {
const visionModel = isVisionModel(options.config.model);
const messages = options.messages.map((v, index) => ({
// "Messages 中 system 角色必须位于列表的最开始"
role: index !== 0 && v.role === "system" ? "user" : v.role,
role: index !== 0 && v.role === 'system' ? 'user' : v.role,
content: visionModel ? v.content : getMessageTextContent(v),
}));
@ -117,7 +118,7 @@ export class HunyuanApi implements LLMApi {
stream: options.config.stream,
});
console.log("[Request] Tencent payload: ", requestPayload);
console.log('[Request] Tencent payload: ', requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
@ -126,7 +127,7 @@ export class HunyuanApi implements LLMApi {
try {
const chatPath = this.path();
const chatPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
@ -139,8 +140,8 @@ export class HunyuanApi implements LLMApi {
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let responseText = '';
let remainText = '';
let finished = false;
let responseRes: Response;
@ -148,9 +149,9 @@ export class HunyuanApi implements LLMApi {
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
console.log('[Response Animation] finished');
if (responseText?.length === 0) {
options.onError?.(new Error("empty response from server"));
options.onError?.(new Error('empty response from server'));
}
return;
}
@ -183,23 +184,23 @@ export class HunyuanApi implements LLMApi {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
const contentType = res.headers.get('content-type');
console.log(
"[Tencent] request response content type: ",
'[Tencent] request response content type: ',
contentType,
);
responseRes = res;
if (contentType?.startsWith("text/plain")) {
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
!res.ok
|| !res.headers
.get('content-type')
?.startsWith(EventStreamContentType)
|| res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
@ -216,13 +217,13 @@ export class HunyuanApi implements LLMApi {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
responseText = responseTexts.join('\n\n');
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
if (msg.data === '[DONE]' || finished) {
return finish();
}
const text = msg.data;
@ -236,7 +237,7 @@ export class HunyuanApi implements LLMApi {
remainText += delta;
}
} catch (e) {
console.error("[Request] parse error", text, msg);
console.error('[Request] parse error', text, msg);
}
},
onclose() {
@ -257,10 +258,11 @@ export class HunyuanApi implements LLMApi {
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
console.log('[Request] failed to make a chat request', e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,

View File

@ -1,25 +1,29 @@
"use client";
'use client';
import type {
ChatMessageTool,
} from '@/app/store';
import type {
ChatOptions,
LLMApi,
LLMModel,
SpeechOptions,
} from '../api';
import type { RequestPayload } from './openai';
import { getClientConfig } from '@/app/config/client';
// azure and openai, using same models. so using same LLMApi.
import { ApiPath, XAI_BASE_URL, XAI, REQUEST_TIMEOUT_MS } from "@/app/constant";
import { ApiPath, REQUEST_TIMEOUT_MS, XAI, XAI_BASE_URL } from '@/app/constant';
import {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
} from '@/app/store';
import { getMessageTextContent } from '@/app/utils';
import { stream } from '@/app/utils/chat';
import { fetch } from '@/app/utils/stream';
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
} from '../api';
export class XAIApi implements LLMApi {
private disableListModels = true;
@ -27,7 +31,7 @@ export class XAIApi implements LLMApi {
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
let baseUrl = '';
if (accessStore.useCustomConfig) {
baseUrl = accessStore.xaiUrl;
@ -39,28 +43,28 @@ export class XAIApi implements LLMApi {
baseUrl = isApp ? XAI_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) {
baseUrl = "https://" + baseUrl;
if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.XAI)) {
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
console.log('[Proxy Endpoint] ', baseUrl, path);
return [baseUrl, path].join("/");
return [baseUrl, path].join('/');
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
return res.choices?.at(0)?.message?.content ?? '';
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
throw new Error('Method not implemented.');
}
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
const messages: ChatOptions['messages'] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
messages.push({ role: v.role, content });
@ -85,7 +89,7 @@ export class XAIApi implements LLMApi {
top_p: modelConfig.top_p,
};
console.log("[Request] xai payload: ", requestPayload);
console.log('[Request] xai payload: ', requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
@ -94,7 +98,7 @@ export class XAIApi implements LLMApi {
try {
const chatPath = this.path(XAI.ChatPath);
const chatPayload = {
method: "POST",
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
@ -145,7 +149,7 @@ export class XAIApi implements LLMApi {
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
runTools[index].function.arguments += args;
}
}
return choices[0]?.delta?.content;
@ -176,10 +180,11 @@ export class XAIApi implements LLMApi {
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
console.log('[Request] failed to make a chat request', e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,

View File

@ -1,6 +1,6 @@
import { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import Locale from "./locales";
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import Locale from './locales';
type Command = (param: string) => void;
interface Commands {
@ -18,7 +18,7 @@ export function useCommand(commands: Commands = {}) {
let shouldUpdate = false;
searchParams.forEach((param, name) => {
const commandName = name as keyof Commands;
if (typeof commands[commandName] === "function") {
if (typeof commands[commandName] === 'function') {
commands[commandName]!(param);
searchParams.delete(name);
shouldUpdate = true;
@ -58,16 +58,16 @@ export function useChatCommand(commands: ChatCommands = {}) {
const input = extract(userInput);
const desc = Locale.Chat.Commands;
return Object.keys(commands)
.filter((c) => c.startsWith(input))
.map((c) => ({
.filter(c => c.startsWith(input))
.map(c => ({
title: desc[c as keyof ChatCommands],
content: ":" + c,
content: `:${c}`,
}));
}
function match(userInput: string) {
const command = extract(userInput);
const matched = typeof commands[command] === "function";
const matched = typeof commands[command] === 'function';
return {
matched,

View File

@ -1,44 +1,44 @@
import { ApiPath, Path, REPO_URL } from '@/app/constant';
import { nanoid } from 'nanoid';
import {
useEffect,
useState,
useRef,
useMemo,
forwardRef,
useEffect,
useImperativeHandle,
} from "react";
import { useParams } from "react-router";
import { IconButton } from "./button";
import { nanoid } from "nanoid";
import ExportIcon from "../icons/share.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
import GithubIcon from "../icons/github.svg";
import LoadingButtonIcon from "../icons/loading.svg";
import ReloadButtonIcon from "../icons/reload.svg";
import Locale from "../locales";
import { Modal, showToast } from "./ui-lib";
import { copyToClipboard, downloadAs } from "../utils";
import { Path, ApiPath, REPO_URL } from "@/app/constant";
import { Loading } from "./home";
import styles from "./artifacts.module.scss";
useMemo,
useRef,
useState,
} from 'react';
import { useParams } from 'react-router';
import CopyIcon from '../icons/copy.svg';
import DownloadIcon from '../icons/download.svg';
import GithubIcon from '../icons/github.svg';
import LoadingButtonIcon from '../icons/loading.svg';
import ReloadButtonIcon from '../icons/reload.svg';
import ExportIcon from '../icons/share.svg';
import Locale from '../locales';
import { copyToClipboard, downloadAs } from '../utils';
import styles from './artifacts.module.scss';
import { IconButton } from './button';
import { Loading } from './home';
import { Modal, showToast } from './ui-lib';
type HTMLPreviewProps = {
interface HTMLPreviewProps {
code: string;
autoHeight?: boolean;
height?: number | string;
onLoad?: (title?: string) => void;
};
}
export type HTMLPreviewHander = {
export interface HTMLPreviewHander {
reload: () => void;
};
}
export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
function HTMLPreview(props, ref) {
(props, ref) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [frameId, setFrameId] = useState<string>(nanoid());
const [iframeHeight, setIframeHeight] = useState(600);
const [title, setTitle] = useState("");
const [title, setTitle] = useState('');
/*
* https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
* 1. using srcdoc
@ -55,9 +55,9 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
setIframeHeight(height);
}
};
window.addEventListener("message", handleMessage);
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
window.removeEventListener('message', handleMessage);
};
}, [frameId]);
@ -68,8 +68,9 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
}));
const height = useMemo(() => {
if (!props.autoHeight) return props.height || 600;
if (typeof props.height === "string") {
if (!props.autoHeight)
{ return props.height || 600; }
if (typeof props.height === 'string') {
return props.height;
}
const parentHeight = props.height || 600;
@ -80,8 +81,8 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
const srcDoc = useMemo(() => {
const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
if (props.code.includes("<!DOCTYPE html>")) {
props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
if (props.code.includes('<!DOCTYPE html>')) {
props.code.replace('<!DOCTYPE html>', `<!DOCTYPE html>${script}`);
}
return script + props.code;
}, [props.code, frameId]);
@ -94,7 +95,7 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
return (
<iframe
className={styles["artifacts-iframe"]}
className={styles['artifacts-iframe']}
key={frameId}
ref={iframeRef}
sandbox="allow-forms allow-modals allow-scripts"
@ -121,22 +122,22 @@ export function ArtifactsShareButton({
const [name, setName] = useState(id);
const [show, setShow] = useState(false);
const shareUrl = useMemo(
() => [location.origin, "#", Path.Artifacts, "/", name].join(""),
() => [location.origin, '#', Path.Artifacts, '/', name].join(''),
[name],
);
const upload = (code: string) =>
id
? Promise.resolve({ id })
: fetch(ApiPath.Artifacts, {
method: "POST",
method: 'POST',
body: code,
})
.then((res) => res.json())
.then(res => res.json())
.then(({ id }) => {
if (id) {
return { id };
}
throw Error();
throw new Error();
})
.catch((e) => {
showToast(Locale.Export.Artifacts.Error);
@ -149,7 +150,8 @@ export function ArtifactsShareButton({
bordered
title={Locale.Export.Artifacts.Title}
onClick={() => {
if (loading) return;
if (loading)
{ return; }
setLoading(true);
upload(getCode())
.then((res) => {
@ -204,9 +206,9 @@ export function ArtifactsShareButton({
export function Artifacts() {
const { id } = useParams();
const [code, setCode] = useState("");
const [code, setCode] = useState('');
const [loading, setLoading] = useState(true);
const [fileName, setFileName] = useState("");
const [fileName, setFileName] = useState('');
const previewRef = useRef<HTMLPreviewHander>(null);
useEffect(() => {
@ -214,11 +216,11 @@ export function Artifacts() {
fetch(`${ApiPath.Artifacts}?id=${id}`)
.then((res) => {
if (res.status > 300) {
throw Error("can not get content");
throw new Error('can not get content');
}
return res;
})
.then((res) => res.text())
.then(res => res.text())
.then(setCode)
.catch((e) => {
showToast(Locale.Export.Artifacts.Error);
@ -227,8 +229,8 @@ export function Artifacts() {
}, [id]);
return (
<div className={styles["artifacts"]}>
<div className={styles["artifacts-header"]}>
<div className={styles.artifacts}>
<div className={styles['artifacts-header']}>
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
<IconButton bordered icon={<GithubIcon />} shadow />
</a>
@ -239,21 +241,21 @@ export function Artifacts() {
shadow
onClick={() => previewRef.current?.reload()}
/>
<div className={styles["artifacts-title"]}>NextChat Artifacts</div>
<div className={styles['artifacts-title']}>NextChat Artifacts</div>
<ArtifactsShareButton
id={id}
getCode={() => code}
fileName={fileName}
/>
</div>
<div className={styles["artifacts-content"]}>
<div className={styles['artifacts-content']}>
{loading && <Loading />}
{code && (
<HTMLPreview
code={code}
ref={previewRef}
autoHeight={false}
height={"100%"}
height="100%"
onLoad={(title) => {
setFileName(title as string);
setLoading(false);

View File

@ -1,24 +1,23 @@
import styles from "./auth.module.scss";
import { IconButton } from "./button";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Path, SAAS_CHAT_URL } from "../constant";
import { useAccessStore } from "../store";
import Locale from "../locales";
import Delete from "../icons/close.svg";
import Arrow from "../icons/arrow.svg";
import Logo from "../icons/logo.svg";
import { useMobileScreen } from "@/app/utils";
import BotIcon from "../icons/bot.svg";
import { getClientConfig } from "../config/client";
import { PasswordInput } from "./ui-lib";
import LeftIcon from "@/app/icons/left.svg";
import { safeLocalStorage } from "@/app/utils";
import LeftIcon from '@/app/icons/left.svg';
import { safeLocalStorage, useMobileScreen } from '@/app/utils';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { getClientConfig } from '../config/client';
import { Path, SAAS_CHAT_URL } from '../constant';
import Arrow from '../icons/arrow.svg';
import BotIcon from '../icons/bot.svg';
import Delete from '../icons/close.svg';
import Logo from '../icons/logo.svg';
import Locale from '../locales';
import { useAccessStore } from '../store';
import {
trackSettingsPageGuideToCPaymentClick,
trackAuthorizationPageButtonToCPaymentClick,
} from "../utils/auth-settings-events";
import clsx from "clsx";
trackSettingsPageGuideToCPaymentClick,
} from '../utils/auth-settings-events';
import styles from './auth.module.scss';
import { IconButton } from './button';
import { PasswordInput } from './ui-lib';
const storage = safeLocalStorage();
@ -34,8 +33,8 @@ export function AuthPage() {
const resetAccessCode = () => {
accessStore.update((access) => {
access.openaiApiKey = "";
access.accessCode = "";
access.openaiApiKey = '';
access.accessCode = '';
});
}; // Reset access code to empty string
@ -47,24 +46,25 @@ export function AuthPage() {
}, []);
return (
<div className={styles["auth-page"]}>
<div className={styles['auth-page']}>
<TopBanner></TopBanner>
<div className={styles["auth-header"]}>
<div className={styles['auth-header']}>
<IconButton
icon={<LeftIcon />}
text={Locale.Auth.Return}
onClick={() => navigate(Path.Home)}
></IconButton>
>
</IconButton>
</div>
<div className={clsx("no-dark", styles["auth-logo"])}>
<div className={clsx('no-dark', styles['auth-logo'])}>
<BotIcon />
</div>
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
<div className={styles['auth-title']}>{Locale.Auth.Title}</div>
<div className={styles['auth-tips']}>{Locale.Auth.Tips}</div>
<PasswordInput
style={{ marginTop: "3vh", marginBottom: "3vh" }}
style={{ marginTop: '3vh', marginBottom: '3vh' }}
aria={Locale.Settings.ShowPassword}
aria-label={Locale.Auth.Input}
value={accessStore.accessCode}
@ -72,44 +72,46 @@ export function AuthPage() {
placeholder={Locale.Auth.Input}
onChange={(e) => {
accessStore.update(
(access) => (access.accessCode = e.currentTarget.value),
access => (access.accessCode = e.currentTarget.value),
);
}}
/>
{!accessStore.hideUserApiKey ? (
<>
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
<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),
);
}}
/>
<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),
);
}}
/>
</>
) : null}
{!accessStore.hideUserApiKey
? (
<>
<div className={styles['auth-tips']}>{Locale.Auth.SubTips}</div>
<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),
);
}}
/>
<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),
);
}}
/>
</>
)
: null}
<div className={styles["auth-actions"]}>
<div className={styles['auth-actions']}>
<IconButton
text={Locale.Auth.Confirm}
type="primary"
@ -132,12 +134,12 @@ function TopBanner() {
const isMobile = useMobileScreen();
useEffect(() => {
// 检查 localStorage 中是否有标记
const bannerDismissed = storage.getItem("bannerDismissed");
const bannerDismissed = storage.getItem('bannerDismissed');
// 如果标记不存在,存储默认值并显示横幅
if (!bannerDismissed) {
storage.setItem("bannerDismissed", "false");
storage.setItem('bannerDismissed', 'false');
setIsVisible(true); // 显示横幅
} else if (bannerDismissed === "true") {
} else if (bannerDismissed === 'true') {
// 如果标记为 "true",则隐藏横幅
setIsVisible(false);
}
@ -153,7 +155,7 @@ function TopBanner() {
const handleClose = () => {
setIsVisible(false);
storage.setItem("bannerDismissed", "true");
storage.setItem('bannerDismissed', 'true');
};
if (!isVisible) {
@ -161,12 +163,12 @@ function TopBanner() {
}
return (
<div
className={styles["top-banner"]}
className={styles['top-banner']}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className={clsx(styles["top-banner-inner"], "no-dark")}>
<Logo className={styles["top-banner-logo"]}></Logo>
<div className={clsx(styles['top-banner-inner'], 'no-dark')}>
<Logo className={styles['top-banner-logo']}></Logo>
<span>
{Locale.Auth.TopTips}
<a
@ -177,12 +179,12 @@ function TopBanner() {
}}
>
{Locale.Settings.Access.SaasStart.ChatNow}
<Arrow style={{ marginLeft: "4px" }} />
<Arrow style={{ marginLeft: '4px' }} />
</a>
</span>
</div>
{(isHovered || isMobile) && (
<Delete className={styles["top-banner-close"]} onClick={handleClose} />
<Delete className={styles['top-banner-close']} onClick={handleClose} />
)}
</div>
);

View File

@ -1,10 +1,10 @@
import * as React from "react";
import type { CSSProperties } from 'react';
import styles from "./button.module.scss";
import { CSSProperties } from "react";
import clsx from "clsx";
import clsx from 'clsx';
import * as React from 'react';
import styles from './button.module.scss';
export type ButtonType = "primary" | "danger" | null;
export type ButtonType = 'primary' | 'danger' | null;
export function IconButton(props: {
onClick?: () => void;
@ -24,13 +24,13 @@ export function IconButton(props: {
return (
<button
className={clsx(
"clickable",
styles["icon-button"],
'clickable',
styles['icon-button'],
{
[styles.border]: props.bordered,
[styles.shadow]: props.shadow,
},
styles[props.type ?? ""],
styles[props.type ?? ''],
props.className,
)}
onClick={props.onClick}
@ -45,8 +45,8 @@ export function IconButton(props: {
{props.icon && (
<div
aria-label={props.text || props.title}
className={clsx(styles["icon-button-icon"], {
"no-dark": props.type === "primary",
className={clsx(styles['icon-button-icon'], {
'no-dark': props.type === 'primary',
})}
>
{props.icon}
@ -56,7 +56,7 @@ export function IconButton(props: {
{props.text && (
<div
aria-label={props.text || props.title}
className={styles["icon-button-text"]}
className={styles['icon-button-text']}
>
{props.text}
</div>

View File

@ -1,24 +1,26 @@
import DeleteIcon from "../icons/delete.svg";
import type {
OnDragEndResponder,
} from '@hello-pangea/dnd';
import type { Mask } from '../store/mask';
import styles from "./home.module.scss";
import {
DragDropContext,
Droppable,
Draggable,
OnDragEndResponder,
} from "@hello-pangea/dnd";
Droppable,
} from '@hello-pangea/dnd';
import clsx from 'clsx';
import { useChatStore } from "../store";
import { useEffect, useRef } from 'react';
import Locale from "../locales";
import { useLocation, useNavigate } from "react-router-dom";
import { Path } from "../constant";
import { MaskAvatar } from "./mask";
import { Mask } from "../store/mask";
import { useRef, useEffect } from "react";
import { showConfirm } from "./ui-lib";
import { useMobileScreen } from "../utils";
import clsx from "clsx";
import { useLocation, useNavigate } from 'react-router-dom';
import { Path } from '../constant';
import DeleteIcon from '../icons/delete.svg';
import Locale from '../locales';
import { useChatStore } from '../store';
import { useMobileScreen } from '../utils';
import styles from './home.module.scss';
import { MaskAvatar } from './mask';
import { showConfirm } from './ui-lib';
export function ChatItem(props: {
onClick?: () => void;
@ -36,7 +38,7 @@ export function ChatItem(props: {
useEffect(() => {
if (props.selected && draggableRef.current) {
draggableRef.current?.scrollIntoView({
block: "center",
block: 'center',
});
}
}, [props.selected]);
@ -44,12 +46,12 @@ export function ChatItem(props: {
const { pathname: currentPath } = useLocation();
return (
<Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => (
{provided => (
<div
className={clsx(styles["chat-item"], {
[styles["chat-item-selected"]]:
props.selected &&
(currentPath === Path.Chat || currentPath === Path.Home),
className={clsx(styles['chat-item'], {
[styles['chat-item-selected']]:
props.selected
&& (currentPath === Path.Chat || currentPath === Path.Home),
})}
onClick={props.onClick}
ref={(ele) => {
@ -62,32 +64,34 @@ export function ChatItem(props: {
props.count,
)}`}
>
{props.narrow ? (
<div className={styles["chat-item-narrow"]}>
<div className={clsx(styles["chat-item-avatar"], "no-dark")}>
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
</div>
<div className={styles["chat-item-narrow-count"]}>
{props.count}
</div>
</div>
) : (
<>
<div className={styles["chat-item-title"]}>{props.title}</div>
<div className={styles["chat-item-info"]}>
<div className={styles["chat-item-count"]}>
{Locale.ChatItem.ChatItemCount(props.count)}
{props.narrow
? (
<div className={styles['chat-item-narrow']}>
<div className={clsx(styles['chat-item-avatar'], 'no-dark')}>
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
</div>
<div className={styles['chat-item-narrow-count']}>
{props.count}
</div>
</div>
<div className={styles["chat-item-date"]}>{props.time}</div>
</div>
</>
)}
)
: (
<>
<div className={styles['chat-item-title']}>{props.title}</div>
<div className={styles['chat-item-info']}>
<div className={styles['chat-item-count']}>
{Locale.ChatItem.ChatItemCount(props.count)}
</div>
<div className={styles['chat-item-date']}>{props.time}</div>
</div>
</>
)}
<div
className={styles["chat-item-delete"]}
className={styles['chat-item-delete']}
onClickCapture={(e) => {
props.onDelete?.();
e.preventDefault();
@ -104,7 +108,7 @@ export function ChatItem(props: {
export function ChatList(props: { narrow?: boolean }) {
const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
(state) => [
state => [
state.sessions,
state.currentSessionIndex,
state.selectSession,
@ -122,8 +126,8 @@ export function ChatList(props: { narrow?: boolean }) {
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
destination.droppableId === source.droppableId
&& destination.index === source.index
) {
return;
}
@ -134,9 +138,9 @@ export function ChatList(props: { narrow?: boolean }) {
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="chat-list">
{(provided) => (
{provided => (
<div
className={styles["chat-list"]}
className={styles['chat-list']}
ref={provided.innerRef}
{...provided.droppableProps}
>
@ -155,8 +159,8 @@ export function ChatList(props: { narrow?: boolean }) {
}}
onDelete={async () => {
if (
(!props.narrow && !isMobileScreen) ||
(await showConfirm(Locale.Home.DeleteChat))
(!props.narrow && !isMobileScreen)
|| (await showConfirm(Locale.Home.DeleteChat))
) {
chatStore.deleteSession(i);
}

View File

@ -235,12 +235,7 @@
animation: slide-in ease 0.3s;
$linear: linear-gradient(
to right,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 1),
rgba(0, 0, 0, 0)
);
$linear: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1), rgba(0, 0, 0, 0));
mask-image: $linear;
@mixin show {
@ -510,13 +505,8 @@
}
@media screen and (min-width: 600px) {
$max-image-width: calc(
calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count)
);
$image-width: calc(
calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 /
var(--image-count)
);
$max-image-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count));
$image-width: calc(calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 / var(--image-count));
.chat-message-item-image-multi {
width: $image-width;

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,15 @@
import type {
EmojiStyle,
} from 'emoji-picker-react';
import type { ModelType } from '../store';
import EmojiPicker, {
Emoji,
EmojiStyle,
Theme as EmojiTheme,
} from "emoji-picker-react";
} from 'emoji-picker-react';
import { ModelType } from "../store";
import BotIcon from "../icons/bot.svg";
import BlackBotIcon from "../icons/black-bot.svg";
import BlackBotIcon from '../icons/black-bot.svg';
import BotIcon from '../icons/bot.svg';
export function getEmojiUrl(unified: string, style: EmojiStyle) {
// Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis
@ -21,7 +23,7 @@ export function AvatarPicker(props: {
}) {
return (
<EmojiPicker
width={"100%"}
width="100%"
lazyLoadEmojis
theme={EmojiTheme.AUTO}
getEmojiUrl={getEmojiUrl}
@ -36,13 +38,15 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
if (props.model) {
return (
<div className="no-dark">
{props.model?.startsWith("gpt-4") ||
props.model?.startsWith("chatgpt-4o") ||
props.model?.startsWith("o1") ? (
<BlackBotIcon className="user-avatar" />
) : (
<BotIcon className="user-avatar" />
)}
{props.model?.startsWith('gpt-4')
|| props.model?.startsWith('chatgpt-4o')
|| props.model?.startsWith('o1')
? (
<BlackBotIcon className="user-avatar" />
)
: (
<BotIcon className="user-avatar" />
)}
</div>
);
}

View File

@ -1,14 +1,14 @@
"use client";
'use client';
import React from "react";
import { IconButton } from "./button";
import GithubIcon from "../icons/github.svg";
import ResetIcon from "../icons/reload.svg";
import { ISSUE_URL } from "../constant";
import Locale from "../locales";
import { showConfirm } from "./ui-lib";
import { useSyncStore } from "../store/sync";
import { useChatStore } from "../store/chat";
import React from 'react';
import { ISSUE_URL } from '../constant';
import GithubIcon from '../icons/github.svg';
import ResetIcon from '../icons/reload.svg';
import Locale from '../locales';
import { useChatStore } from '../store/chat';
import { useSyncStore } from '../store/sync';
import { IconButton } from './button';
import { showConfirm } from './ui-lib';
interface IErrorBoundaryState {
hasError: boolean;
@ -46,7 +46,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
<code>{this.state.info?.componentStack}</code>
</pre>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<a href={ISSUE_URL} className="report">
<IconButton
text="Report This Error"

View File

@ -206,7 +206,7 @@
}
@media screen and (max-width: 600px) {
$image-width: calc(calc(100vw/2)/var(--image-count));
$image-width: calc(calc(100vw / 2) / var(--image-count));
.message-image-multi {
width: $image-width;
@ -214,13 +214,13 @@
}
.message-image {
max-width: calc(100vw/3*2);
max-width: calc(100vw / 3 * 2);
}
}
@media screen and (min-width: 600px) {
$max-image-width: calc(900px/3*2/var(--image-count));
$image-width: calc(80vw/3*2/var(--image-count));
$max-image-width: calc(900px / 3 * 2 / var(--image-count));
$image-width: calc(80vw / 3 * 2 / var(--image-count));
.message-image-multi {
width: $image-width;
@ -230,7 +230,7 @@
}
.message-image {
max-width: calc(100vw/3*2);
max-width: calc(100vw / 3 * 2);
}
}
@ -267,5 +267,6 @@
}
}
.default-theme {}
}
.default-theme {
}
}

View File

@ -1,7 +1,37 @@
/* eslint-disable @next/next/no-img-element */
import { ChatMessage, useAppConfig, useChatStore } from "../store";
import Locale from "../locales";
import styles from "./exporter.module.scss";
import type { ChatMessage } from '../store';
import clsx from 'clsx';
import { toBlob, toPng } from 'html-to-image';
import dynamic from 'next/dynamic';
import NextImage from 'next/image';
import { useEffect, useMemo, useRef, useState } from 'react';
import { type ClientApi, getClientApi } from '../client/api';
import { getClientConfig } from '../config/client';
import { EXPORT_MESSAGE_CLASS_NAME } from '../constant';
import BotIcon from '../icons/bot.png';
import ChatGptIcon from '../icons/chatgpt.png';
import CopyIcon from '../icons/copy.svg';
import DownloadIcon from '../icons/download.svg';
import ShareIcon from '../icons/share.svg';
import LoadingIcon from '../icons/three-dots.svg';
import Locale from '../locales';
import { useAppConfig, useChatStore } from '../store';
import { DEFAULT_MASK_AVATAR } from '../store/mask';
import {
copyToClipboard,
downloadAs,
getMessageImages,
getMessageTextContent,
useMobileScreen,
} from '../utils';
import { prettyObject } from '../utils/format';
import { IconButton } from './button';
import { Avatar } from './emoji';
import styles from './exporter.module.scss';
import { MessageSelector, useMessageSelector } from './message-selector';
import {
List,
ListItem,
@ -10,39 +40,9 @@ import {
showImageModal,
showModal,
showToast,
} from "./ui-lib";
import { IconButton } from "./button";
import {
copyToClipboard,
downloadAs,
getMessageImages,
useMobileScreen,
} from "../utils";
} from './ui-lib';
import CopyIcon from "../icons/copy.svg";
import LoadingIcon from "../icons/three-dots.svg";
import ChatGptIcon from "../icons/chatgpt.png";
import ShareIcon from "../icons/share.svg";
import BotIcon from "../icons/bot.png";
import DownloadIcon from "../icons/download.svg";
import { useEffect, useMemo, useRef, useState } from "react";
import { MessageSelector, useMessageSelector } from "./message-selector";
import { Avatar } from "./emoji";
import dynamic from "next/dynamic";
import NextImage from "next/image";
import { toBlob, toPng } from "html-to-image";
import { DEFAULT_MASK_AVATAR } from "../store/mask";
import { prettyObject } from "../utils/format";
import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
import { getClientConfig } from "../config/client";
import { type ClientApi, getClientApi } from "../client/api";
import { getMessageTextContent } from "../utils";
import clsx from "clsx";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
const Markdown = dynamic(async () => (await import('./markdown')).Markdown, {
loading: () => <LoadingIcon />,
});
@ -52,20 +52,20 @@ export function ExportMessageModal(props: { onClose: () => void }) {
<Modal
title={Locale.Export.Title}
onClose={props.onClose}
footer={
footer={(
<div
style={{
width: "100%",
textAlign: "center",
width: '100%',
textAlign: 'center',
fontSize: 14,
opacity: 0.5,
}}
>
{Locale.Exporter.Description.Title}
</div>
}
)}
>
<div style={{ minHeight: "40vh" }}>
<div style={{ minHeight: '40vh' }}>
<MessageExporter />
</div>
</Modal>
@ -105,31 +105,32 @@ function Steps<
const stepCount = steps.length;
return (
<div className={styles["steps"]}>
<div className={styles["steps-progress"]}>
<div className={styles.steps}>
<div className={styles['steps-progress']}>
<div
className={styles["steps-progress-inner"]}
className={styles['steps-progress-inner']}
style={{
width: `${((props.index + 1) / stepCount) * 100}%`,
}}
></div>
>
</div>
</div>
<div className={styles["steps-inner"]}>
<div className={styles['steps-inner']}>
{steps.map((step, i) => {
return (
<div
key={i}
className={clsx("clickable", styles["step"], {
[styles["step-finished"]]: i <= props.index,
[styles["step-current"]]: i === props.index,
className={clsx('clickable', styles.step, {
[styles['step-finished']]: i <= props.index,
[styles['step-current']]: i === props.index,
})}
onClick={() => {
props.onStepChange?.(i);
}}
role="button"
>
<span className={styles["step-index"]}>{i + 1}</span>
<span className={styles["step-name"]}>{step.name}</span>
<span className={styles['step-index']}>{i + 1}</span>
<span className={styles['step-name']}>{step.name}</span>
</div>
);
})}
@ -142,20 +143,20 @@ export function MessageExporter() {
const steps = [
{
name: Locale.Export.Steps.Select,
value: "select",
value: 'select',
},
{
name: Locale.Export.Steps.Preview,
value: "preview",
value: 'preview',
},
];
const { currentStep, setCurrentStepIndex, currentStepIndex } =
useSteps(steps);
const formats = ["text", "image", "json"] as const;
const { currentStep, setCurrentStepIndex, currentStepIndex }
= useSteps(steps);
const formats = ['text', 'image', 'json'] as const;
type ExportFormat = (typeof formats)[number];
const [exportConfig, setExportConfig] = useState({
format: "image" as ExportFormat,
format: 'image' as ExportFormat,
includeContext: true,
});
@ -173,7 +174,7 @@ export function MessageExporter() {
if (exportConfig.includeContext) {
ret.push(...session.mask.context);
}
ret.push(...session.messages.filter((m) => selection.has(m.id)));
ret.push(...session.messages.filter(m => selection.has(m.id)));
return ret;
}, [
exportConfig.includeContext,
@ -182,11 +183,11 @@ export function MessageExporter() {
selection,
]);
function preview() {
if (exportConfig.format === "text") {
if (exportConfig.format === 'text') {
return (
<MarkdownPreviewer messages={selectedMessages} topic={session.topic} />
);
} else if (exportConfig.format === "json") {
} else if (exportConfig.format === 'json') {
return (
<JsonPreviewer messages={selectedMessages} topic={session.topic} />
);
@ -204,8 +205,8 @@ export function MessageExporter() {
onStepChange={setCurrentStepIndex}
/>
<div
className={styles["message-exporter-body"]}
style={currentStep.value !== "select" ? { display: "none" } : {}}
className={styles['message-exporter-body']}
style={currentStep.value !== 'select' ? { display: 'none' } : {}}
>
<List>
<ListItem
@ -214,14 +215,13 @@ export function MessageExporter() {
>
<Select
value={exportConfig.format}
onChange={(e) =>
onChange={e =>
updateExportConfig(
(config) =>
config =>
(config.format = e.currentTarget.value as ExportFormat),
)
}
)}
>
{formats.map((f) => (
{formats.map(f => (
<option key={f} value={f}>
{f}
</option>
@ -237,10 +237,11 @@ export function MessageExporter() {
checked={exportConfig.includeContext}
onChange={(e) => {
updateExportConfig(
(config) => (config.includeContext = e.currentTarget.checked),
config => (config.includeContext = e.currentTarget.checked),
);
}}
></input>
>
</input>
</ListItem>
</List>
<MessageSelector
@ -249,8 +250,8 @@ export function MessageExporter() {
defaultSelectAll
/>
</div>
{currentStep.value === "preview" && (
<div className={styles["message-exporter-body"]}>{preview()}</div>
{currentStep.value === 'preview' && (
<div className={styles['message-exporter-body']}>{preview()}</div>
)}
</>
);
@ -263,7 +264,8 @@ export function RenderExport(props: {
const domRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!domRef.current) return;
if (!domRef.current)
{ return; }
const dom = domRef.current;
const messages = Array.from(
dom.getElementsByClassName(EXPORT_MESSAGE_CLASS_NAME),
@ -274,12 +276,12 @@ export function RenderExport(props: {
}
const renderMsgs = messages.map((v, i) => {
const [role, _] = v.id.split(":");
const [role, _] = v.id.split(':');
return {
id: i.toString(),
role: role as any,
content: role === "user" ? v.textContent ?? "" : v.innerHTML,
date: "",
content: role === 'user' ? v.textContent ?? '' : v.innerHTML,
date: '',
};
});
@ -319,7 +321,8 @@ export function PreviewActions(props: {
api
.share(msgs)
.then((res) => {
if (!res) return;
if (!res)
{ return; }
showModal({
title: Locale.Export.Share,
children: [
@ -328,12 +331,13 @@ export function PreviewActions(props: {
value={res}
key="input"
style={{
width: "100%",
maxWidth: "unset",
width: '100%',
maxWidth: 'unset',
}}
readOnly
onClick={(e) => e.currentTarget.select()}
></input>,
onClick={e => e.currentTarget.select()}
>
</input>,
],
actions: [
<IconButton
@ -345,11 +349,11 @@ export function PreviewActions(props: {
],
});
setTimeout(() => {
window.open(res, "_blank");
window.open(res, '_blank');
}, 800);
})
.catch((e) => {
console.error("[Share]", e);
console.error('[Share]', e);
showToast(prettyObject(e));
})
.finally(() => setLoading(false));
@ -364,7 +368,7 @@ export function PreviewActions(props: {
return (
<>
<div className={styles["preview-actions"]}>
<div className={styles['preview-actions']}>
{props.showCopy && (
<IconButton
text={Locale.Export.Copy}
@ -372,7 +376,8 @@ export function PreviewActions(props: {
shadow
icon={<CopyIcon />}
onClick={props.copy}
></IconButton>
>
</IconButton>
)}
<IconButton
text={Locale.Export.Download}
@ -380,20 +385,22 @@ export function PreviewActions(props: {
shadow
icon={<DownloadIcon />}
onClick={props.download}
></IconButton>
>
</IconButton>
<IconButton
text={Locale.Export.Share}
bordered
shadow
icon={loading ? <LoadingIcon /> : <ShareIcon />}
onClick={share}
></IconButton>
>
</IconButton>
</div>
<div
style={{
position: "fixed",
right: "200vw",
pointerEvents: "none",
position: 'fixed',
right: '200vw',
pointerEvents: 'none',
}}
>
{shouldExport && (
@ -437,14 +444,16 @@ export function ImagePreviewer(props: {
const copy = () => {
showToast(Locale.Export.Image.Toast);
const dom = previewRef.current;
if (!dom) return;
if (!dom)
{ return; }
toBlob(dom).then((blob) => {
if (!blob) return;
if (!blob)
{ return; }
try {
navigator.clipboard
.write([
new ClipboardItem({
"image/png": blob,
'image/png': blob,
}),
])
.then(() => {
@ -452,7 +461,7 @@ export function ImagePreviewer(props: {
refreshPreview();
});
} catch (e) {
console.error("[Copy Image] ", e);
console.error('[Copy Image] ', e);
showToast(Locale.Copy.Failed);
}
});
@ -463,13 +472,15 @@ export function ImagePreviewer(props: {
const download = async () => {
showToast(Locale.Export.Image.Toast);
const dom = previewRef.current;
if (!dom) return;
if (!dom)
{ return; }
const isApp = getClientConfig()?.isApp;
try {
const blob = await toPng(dom);
if (!blob) return;
if (!blob)
{ return; }
if (isMobile || (isApp && window.__TAURI__)) {
if (isApp && window.__TAURI__) {
@ -477,12 +488,12 @@ export function ImagePreviewer(props: {
defaultPath: `${props.topic}.png`,
filters: [
{
name: "PNG Files",
extensions: ["png"],
name: 'PNG Files',
extensions: ['png'],
},
{
name: "All Files",
extensions: ["*"],
name: 'All Files',
extensions: ['*'],
},
],
});
@ -500,7 +511,7 @@ export function ImagePreviewer(props: {
showImageModal(blob);
}
} else {
const link = document.createElement("a");
const link = document.createElement('a');
link.download = `${props.topic}.png`;
link.href = blob;
link.click();
@ -519,7 +530,7 @@ export function ImagePreviewer(props: {
};
return (
<div className={styles["image-previewer"]}>
<div className={styles['image-previewer']}>
<PreviewActions
copy={copy}
download={download}
@ -527,11 +538,11 @@ export function ImagePreviewer(props: {
messages={props.messages}
/>
<div
className={clsx(styles["preview-body"], styles["default-theme"])}
className={clsx(styles['preview-body'], styles['default-theme'])}
ref={previewRef}
>
<div className={styles["chat-info"]}>
<div className={clsx(styles["logo"], "no-dark")}>
<div className={styles['chat-info']}>
<div className={clsx(styles.logo, 'no-dark')}>
<NextImage
src={ChatGptIcon.src}
alt="logo"
@ -541,28 +552,36 @@ export function ImagePreviewer(props: {
</div>
<div>
<div className={styles["main-title"]}>NextChat</div>
<div className={styles["sub-title"]}>
<div className={styles['main-title']}>NextChat</div>
<div className={styles['sub-title']}>
github.com/ChatGPTNextWeb/ChatGPT-Next-Web
</div>
<div className={styles["icons"]}>
<div className={styles.icons}>
<ExportAvatar avatar={config.avatar} />
<span className={styles["icon-space"]}>&</span>
<span className={styles['icon-space']}>&</span>
<ExportAvatar avatar={mask.avatar} />
</div>
</div>
<div>
<div className={styles["chat-info-item"]}>
{Locale.Exporter.Model}: {mask.modelConfig.model}
<div className={styles['chat-info-item']}>
{Locale.Exporter.Model}
:
{mask.modelConfig.model}
</div>
<div className={styles["chat-info-item"]}>
{Locale.Exporter.Messages}: {props.messages.length}
<div className={styles['chat-info-item']}>
{Locale.Exporter.Messages}
:
{props.messages.length}
</div>
<div className={styles["chat-info-item"]}>
{Locale.Exporter.Topic}: {session.topic}
<div className={styles['chat-info-item']}>
{Locale.Exporter.Topic}
:
{session.topic}
</div>
<div className={styles["chat-info-item"]}>
{Locale.Exporter.Time}:{" "}
<div className={styles['chat-info-item']}>
{Locale.Exporter.Time}
:
{' '}
{new Date(
props.messages.at(-1)?.date ?? Date.now(),
).toLocaleString()}
@ -572,16 +591,16 @@ export function ImagePreviewer(props: {
{props.messages.map((m, i) => {
return (
<div
className={clsx(styles["message"], styles["message-" + m.role])}
className={clsx(styles.message, styles[`message-${m.role}`])}
key={i}
>
<div className={styles["avatar"]}>
<div className={styles.avatar}>
<ExportAvatar
avatar={m.role === "user" ? config.avatar : mask.avatar}
avatar={m.role === 'user' ? config.avatar : mask.avatar}
/>
</div>
<div className={styles["body"]}>
<div className={styles.body}>
<Markdown
content={getMessageTextContent(m)}
fontSize={config.fontSize}
@ -593,15 +612,15 @@ export function ImagePreviewer(props: {
key={i}
src={getMessageImages(m)[0]}
alt="message"
className={styles["message-image"]}
className={styles['message-image']}
/>
)}
{getMessageImages(m).length > 1 && (
<div
className={styles["message-images"]}
className={styles['message-images']}
style={
{
"--image-count": getMessageImages(m).length,
'--image-count': getMessageImages(m).length,
} as React.CSSProperties
}
>
@ -610,7 +629,7 @@ export function ImagePreviewer(props: {
key={i}
src={src}
alt="message"
className={styles["message-image-multi"]}
className={styles['message-image-multi']}
/>
))}
</div>
@ -628,17 +647,17 @@ export function MarkdownPreviewer(props: {
messages: ChatMessage[];
topic: string;
}) {
const mdText =
`# ${props.topic}\n\n` +
props.messages
.map((m) => {
return m.role === "user"
? `## ${Locale.Export.MessageFromYou}:\n${getMessageTextContent(m)}`
: `## ${Locale.Export.MessageFromChatGPT}:\n${getMessageTextContent(
const mdText
= `# ${props.topic}\n\n${
props.messages
.map((m) => {
return m.role === 'user'
? `## ${Locale.Export.MessageFromYou}:\n${getMessageTextContent(m)}`
: `## ${Locale.Export.MessageFromChatGPT}:\n${getMessageTextContent(
m,
).trim()}`;
})
.join("\n\n");
})
.join('\n\n')}`;
const copy = () => {
copyToClipboard(mdText);
@ -651,11 +670,11 @@ export function MarkdownPreviewer(props: {
<PreviewActions
copy={copy}
download={download}
showCopy={true}
showCopy
messages={props.messages}
/>
<div className="markdown-body">
<pre className={styles["export-content"]}>{mdText}</pre>
<pre className={styles['export-content']}>{mdText}</pre>
</div>
</>
);
@ -668,16 +687,16 @@ export function JsonPreviewer(props: {
const msgs = {
messages: [
{
role: "system",
role: 'system',
content: `${Locale.FineTuned.Sysmessage} ${props.topic}`,
},
...props.messages.map((m) => ({
...props.messages.map(m => ({
role: m.role,
content: m.content,
})),
],
};
const mdText = "```json\n" + JSON.stringify(msgs, null, 2) + "\n```";
const mdText = `\`\`\`json\n${JSON.stringify(msgs, null, 2)}\n\`\`\``;
const minifiedJson = JSON.stringify(msgs);
const copy = () => {

View File

@ -1,76 +1,76 @@
"use client";
require("../polyfill");
import { useState, useEffect } from "react";
import styles from "./home.module.scss";
import BotIcon from "../icons/bot.svg";
import LoadingIcon from "../icons/three-dots.svg";
import { getCSSVar, useMobileScreen } from "../utils";
import dynamic from "next/dynamic";
import { Path, SlotID } from "../constant";
import { ErrorBoundary } from "./error";
import { getISOLang, getLang } from "../locales";
import clsx from 'clsx';
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
import {
Route,
HashRouter as Router,
Routes,
Route,
useLocation,
} from "react-router-dom";
import { SideBar } from "./sidebar";
import { useAppConfig } from "../store/config";
import { AuthPage } from "./auth";
import { getClientConfig } from "../config/client";
import { type ClientApi, getClientApi } from "../client/api";
import { useAccessStore } from "../store";
import clsx from "clsx";
} from 'react-router-dom';
import { type ClientApi, getClientApi } from '../client/api';
import { getClientConfig } from '../config/client';
import { Path, SlotID } from '../constant';
import BotIcon from '../icons/bot.svg';
import LoadingIcon from '../icons/three-dots.svg';
import { getISOLang, getLang } from '../locales';
import { useAccessStore } from '../store';
import { useAppConfig } from '../store/config';
import { getCSSVar, useMobileScreen } from '../utils';
import { AuthPage } from './auth';
import { ErrorBoundary } from './error';
import styles from './home.module.scss';
import { SideBar } from './sidebar';
'use client';
require('../polyfill');
export function Loading(props: { noLogo?: boolean }) {
return (
<div className={clsx("no-dark", styles["loading-content"])}>
<div className={clsx('no-dark', styles['loading-content'])}>
{!props.noLogo && <BotIcon />}
<LoadingIcon />
</div>
);
}
const Artifacts = dynamic(async () => (await import("./artifacts")).Artifacts, {
const Artifacts = dynamic(async () => (await import('./artifacts')).Artifacts, {
loading: () => <Loading noLogo />,
});
const Settings = dynamic(async () => (await import("./settings")).Settings, {
const Settings = dynamic(async () => (await import('./settings')).Settings, {
loading: () => <Loading noLogo />,
});
const Chat = dynamic(async () => (await import("./chat")).Chat, {
const Chat = dynamic(async () => (await import('./chat')).Chat, {
loading: () => <Loading noLogo />,
});
const NewChat = dynamic(async () => (await import("./new-chat")).NewChat, {
const NewChat = dynamic(async () => (await import('./new-chat')).NewChat, {
loading: () => <Loading noLogo />,
});
const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
const MaskPage = dynamic(async () => (await import('./mask')).MaskPage, {
loading: () => <Loading noLogo />,
});
const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
const PluginPage = dynamic(async () => (await import('./plugin')).PluginPage, {
loading: () => <Loading noLogo />,
});
const SearchChat = dynamic(
async () => (await import("./search-chat")).SearchChatPage,
async () => (await import('./search-chat')).SearchChatPage,
{
loading: () => <Loading noLogo />,
},
);
const Sd = dynamic(async () => (await import("./sd")).Sd, {
const Sd = dynamic(async () => (await import('./sd')).Sd, {
loading: () => <Loading noLogo />,
});
@ -78,13 +78,13 @@ export function useSwitchTheme() {
const config = useAppConfig();
useEffect(() => {
document.body.classList.remove("light");
document.body.classList.remove("dark");
document.body.classList.remove('light');
document.body.classList.remove('dark');
if (config.theme === "dark") {
document.body.classList.add("dark");
} else if (config.theme === "light") {
document.body.classList.add("light");
if (config.theme === 'dark') {
document.body.classList.add('dark');
} else if (config.theme === 'light') {
document.body.classList.add('light');
}
const metaDescriptionDark = document.querySelector(
@ -94,13 +94,13 @@ export function useSwitchTheme() {
'meta[name="theme-color"][media*="light"]',
);
if (config.theme === "auto") {
metaDescriptionDark?.setAttribute("content", "#151515");
metaDescriptionLight?.setAttribute("content", "#fafafa");
if (config.theme === 'auto') {
metaDescriptionDark?.setAttribute('content', '#151515');
metaDescriptionLight?.setAttribute('content', '#fafafa');
} else {
const themeColor = getCSSVar("--theme-color");
metaDescriptionDark?.setAttribute("content", themeColor);
metaDescriptionLight?.setAttribute("content", themeColor);
const themeColor = getCSSVar('--theme-color');
metaDescriptionDark?.setAttribute('content', themeColor);
metaDescriptionLight?.setAttribute('content', themeColor);
}
}, [config.theme]);
}
@ -116,7 +116,7 @@ function useHtmlLang() {
}, []);
}
const useHasHydrated = () => {
function useHasHydrated() {
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
useEffect(() => {
@ -124,26 +124,26 @@ const useHasHydrated = () => {
}, []);
return hasHydrated;
};
}
const loadAsyncGoogleFont = () => {
const linkEl = document.createElement("link");
const proxyFontUrl = "/google-fonts";
const remoteFontUrl = "https://fonts.googleapis.com";
const googleFontUrl =
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
linkEl.rel = "stylesheet";
linkEl.href =
googleFontUrl +
"/css2?family=" +
encodeURIComponent("Noto Sans:wght@300;400;700;900") +
"&display=swap";
function loadAsyncGoogleFont() {
const linkEl = document.createElement('link');
const proxyFontUrl = '/google-fonts';
const remoteFontUrl = 'https://fonts.googleapis.com';
const googleFontUrl
= getClientConfig()?.buildMode === 'export' ? remoteFontUrl : proxyFontUrl;
linkEl.rel = 'stylesheet';
linkEl.href
= `${googleFontUrl
}/css2?family=${
encodeURIComponent('Noto Sans:wght@300;400;700;900')
}&display=swap`;
document.head.appendChild(linkEl);
};
}
export function WindowContent(props: { children: React.ReactNode }) {
return (
<div className={styles["window-content"]} id={SlotID.AppBody}>
<div className={styles['window-content']} id={SlotID.AppBody}>
{props?.children}
</div>
);
@ -159,8 +159,8 @@ function Screen() {
const isSdNew = location.pathname === Path.SdNew;
const isMobileScreen = useMobileScreen();
const shouldTightBorder =
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
const shouldTightBorder
= getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
useEffect(() => {
loadAsyncGoogleFont();
@ -174,14 +174,17 @@ function Screen() {
);
}
const renderContent = () => {
if (isAuth) return <AuthPage />;
if (isSd) return <Sd />;
if (isSdNew) return <Sd />;
if (isAuth)
{ return <AuthPage />; }
if (isSd)
{ return <Sd />; }
if (isSdNew)
{ return <Sd />; }
return (
<>
<SideBar
className={clsx({
[styles["sidebar-show"]]: isHome,
[styles['sidebar-show']]: isHome,
})}
/>
<WindowContent>
@ -202,8 +205,8 @@ function Screen() {
return (
<div
className={clsx(styles.container, {
[styles["tight-container"]]: shouldTightBorder,
[styles["rtl-screen"]]: getLang() === "ar",
[styles['tight-container']]: shouldTightBorder,
[styles['rtl-screen']]: getLang() === 'ar',
})}
>
{renderContent()}
@ -231,7 +234,7 @@ export function Home() {
useHtmlLang();
useEffect(() => {
console.log("[Config] got config from build time", getClientConfig());
console.log('[Config] got config from build time', getClientConfig());
useAccessStore.getState().fetch();
}, []);

View File

@ -1,6 +1,6 @@
import * as React from "react";
import styles from "./input-range.module.scss";
import clsx from "clsx";
import clsx from 'clsx';
import * as React from 'react';
import styles from './input-range.module.scss';
interface InputRangeProps {
onChange: React.ChangeEventHandler<HTMLInputElement>;
@ -24,7 +24,7 @@ export function InputRange({
aria,
}: InputRangeProps) {
return (
<div className={clsx(styles["input-range"], className)}>
<div className={clsx(styles['input-range'], className)}>
{title || value}
<input
aria-label={aria}
@ -35,7 +35,8 @@ export function InputRange({
max={max}
step={step}
onChange={onChange}
></input>
>
</input>
</div>
);
}

View File

@ -1,29 +1,31 @@
import ReactMarkdown from "react-markdown";
import "katex/dist/katex.min.css";
import RemarkMath from "remark-math";
import RemarkBreaks from "remark-breaks";
import RehypeKatex from "rehype-katex";
import RemarkGfm from "remark-gfm";
import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
import { copyToClipboard, useWindowSize } from "../utils";
import mermaid from "mermaid";
import Locale from "../locales";
import LoadingIcon from "../icons/three-dots.svg";
import ReloadButtonIcon from "../icons/reload.svg";
import React from "react";
import { useDebouncedCallback } from "use-debounce";
import { showImageModal, FullScreen } from "./ui-lib";
import type { RefObject } from 'react';
import type {
HTMLPreviewHander,
} from './artifacts';
import clsx from 'clsx';
import mermaid from 'mermaid';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import RehypeHighlight from 'rehype-highlight';
import RehypeKatex from 'rehype-katex';
import RemarkBreaks from 'remark-breaks';
import RemarkGfm from 'remark-gfm';
import RemarkMath from 'remark-math';
import { useDebouncedCallback } from 'use-debounce';
import ReloadButtonIcon from '../icons/reload.svg';
import LoadingIcon from '../icons/three-dots.svg';
import Locale from '../locales';
import { useChatStore } from '../store';
import { useAppConfig } from '../store/config';
import { copyToClipboard, useWindowSize } from '../utils';
import {
ArtifactsShareButton,
HTMLPreview,
HTMLPreviewHander,
} from "./artifacts";
import { useChatStore } from "../store";
import { IconButton } from "./button";
} from './artifacts';
import { IconButton } from './button';
import { useAppConfig } from "../store/config";
import clsx from "clsx";
import { FullScreen, showImageModal } from './ui-lib';
import 'katex/dist/katex.min.css';
export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null);
@ -38,17 +40,17 @@ export function Mermaid(props: { code: string }) {
})
.catch((e) => {
setHasError(true);
console.error("[Mermaid] ", e.message);
console.error('[Mermaid] ', e.message);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.code]);
function viewSvgInNewWindow() {
const svg = ref.current?.querySelector("svg");
if (!svg) return;
const svg = ref.current?.querySelector('svg');
if (!svg)
{ return; }
const text = new XMLSerializer().serializeToString(svg);
const blob = new Blob([text], { type: "image/svg+xml" });
const blob = new Blob([text], { type: 'image/svg+xml' });
showImageModal(URL.createObjectURL(blob));
}
@ -58,10 +60,10 @@ export function Mermaid(props: { code: string }) {
return (
<div
className={clsx("no-dark", "mermaid")}
className={clsx('no-dark', 'mermaid')}
style={{
cursor: "pointer",
overflow: "auto",
cursor: 'pointer',
overflow: 'auto',
}}
ref={ref}
onClick={() => viewSvgInNewWindow()}
@ -74,56 +76,57 @@ export function Mermaid(props: { code: string }) {
export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null);
const previewRef = useRef<HTMLPreviewHander>(null);
const [mermaidCode, setMermaidCode] = useState("");
const [htmlCode, setHtmlCode] = useState("");
const [mermaidCode, setMermaidCode] = useState('');
const [htmlCode, setHtmlCode] = useState('');
const { height } = useWindowSize();
const chatStore = useChatStore();
const session = chatStore.currentSession();
const renderArtifacts = useDebouncedCallback(() => {
if (!ref.current) return;
const mermaidDom = ref.current.querySelector("code.language-mermaid");
if (!ref.current)
{ return; }
const mermaidDom = ref.current.querySelector('code.language-mermaid');
if (mermaidDom) {
setMermaidCode((mermaidDom as HTMLElement).innerText);
}
const htmlDom = ref.current.querySelector("code.language-html");
const refText = ref.current.querySelector("code")?.innerText;
const htmlDom = ref.current.querySelector('code.language-html');
const refText = ref.current.querySelector('code')?.innerText;
if (htmlDom) {
setHtmlCode((htmlDom as HTMLElement).innerText);
} else if (
refText?.startsWith("<!DOCTYPE") ||
refText?.startsWith("<svg") ||
refText?.startsWith("<?xml")
refText?.startsWith('<!DOCTYPE')
|| refText?.startsWith('<svg')
|| refText?.startsWith('<?xml')
) {
setHtmlCode(refText);
}
}, 600);
const config = useAppConfig();
const enableArtifacts =
session.mask?.enableArtifacts !== false && config.enableArtifacts;
const enableArtifacts
= session.mask?.enableArtifacts !== false && config.enableArtifacts;
//Wrap the paragraph for plain-text
// Wrap the paragraph for plain-text
useEffect(() => {
if (ref.current) {
const codeElements = ref.current.querySelectorAll(
"code",
'code',
) as NodeListOf<HTMLElement>;
const wrapLanguages = [
"",
"md",
"markdown",
"text",
"txt",
"plaintext",
"tex",
"latex",
'',
'md',
'markdown',
'text',
'txt',
'plaintext',
'tex',
'latex',
];
codeElements.forEach((codeElement) => {
let languageClass = codeElement.className.match(/language-(\w+)/);
let name = languageClass ? languageClass[1] : "";
const languageClass = codeElement.className.match(/language-(\w+)/);
const name = languageClass ? languageClass[1] : '';
if (wrapLanguages.includes(name)) {
codeElement.style.whiteSpace = "pre-wrap";
codeElement.style.whiteSpace = 'pre-wrap';
}
});
setTimeout(renderArtifacts, 1);
@ -138,11 +141,12 @@ export function PreCode(props: { children: any }) {
onClick={() => {
if (ref.current) {
copyToClipboard(
ref.current.querySelector("code")?.innerText ?? "",
ref.current.querySelector('code')?.innerText ?? '',
);
}
}}
></span>
>
</span>
{props.children}
</pre>
{mermaidCode.length > 0 && (
@ -151,11 +155,11 @@ export function PreCode(props: { children: any }) {
{htmlCode.length > 0 && enableArtifacts && (
<FullScreen className="no-dark html" right={70}>
<ArtifactsShareButton
style={{ position: "absolute", right: 20, top: 10 }}
style={{ position: 'absolute', right: 20, top: 10 }}
getCode={() => htmlCode}
/>
<IconButton
style={{ position: "absolute", right: 120, top: 10 }}
style={{ position: 'absolute', right: 120, top: 10 }}
bordered
icon={<ReloadButtonIcon />}
shadow
@ -177,8 +181,8 @@ function CustomCode(props: { children: any; className?: string }) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const config = useAppConfig();
const enableCodeFold =
session.mask?.enableCodeFold !== false && config.enableCodeFold;
const enableCodeFold
= session.mask?.enableCodeFold !== false && config.enableCodeFold;
const ref = useRef<HTMLPreElement>(null);
const [collapsed, setCollapsed] = useState(true);
@ -193,13 +197,13 @@ function CustomCode(props: { children: any; className?: string }) {
}, [props.children]);
const toggleCollapsed = () => {
setCollapsed((collapsed) => !collapsed);
setCollapsed(collapsed => !collapsed);
};
const renderShowMoreButton = () => {
if (showToggle && enableCodeFold && collapsed) {
return (
<div
className={clsx("show-hide-button", {
className={clsx('show-hide-button', {
collapsed,
expanded: !collapsed,
})}
@ -216,8 +220,8 @@ function CustomCode(props: { children: any; className?: string }) {
className={clsx(props?.className)}
ref={ref}
style={{
maxHeight: enableCodeFold && collapsed ? "400px" : "none",
overflowY: "hidden",
maxHeight: enableCodeFold && collapsed ? '400px' : 'none',
overflowY: 'hidden',
}}
>
{props.children}
@ -229,8 +233,8 @@ function CustomCode(props: { children: any; className?: string }) {
}
function escapeBrackets(text: string) {
const pattern =
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
const pattern
= /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
return text.replace(
pattern,
(match, codeBlock, squareBracket, roundBracket) => {
@ -249,20 +253,20 @@ function escapeBrackets(text: string) {
function tryWrapHtmlCode(text: string) {
// try add wrap html code (fixed: html codeblock include 2 newline)
// ignore embed codeblock
if (text.includes("```")) {
if (text.includes('```')) {
return text;
}
return text
.replace(
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
/(`*)(\w*)([\n\r]*)(<!DOCTYPE html>)/g,
(match, quoteStart, lang, newLine, doctype) => {
return !quoteStart ? "\n```html\n" + doctype : match;
return !quoteStart ? `\n\`\`\`html\n${doctype}` : match;
},
)
.replace(
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
/(<\/body>)(\s*)(<\/html>)([\n\r]*)(`*)([\n\r]*?)/g,
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
return !quoteEnd ? `${bodyEnd + space + htmlEnd}\n\`\`\`\n` : match;
},
);
}
@ -288,9 +292,9 @@ function _MarkDownContent(props: { content: string }) {
components={{
pre: PreCode,
code: CustomCode,
p: (pProps) => <p {...pProps} dir="auto" />,
p: pProps => <p {...pProps} dir="auto" />,
a: (aProps) => {
const href = aProps.href || "";
const href = aProps.href || '';
if (/\.(aac|mp3|opus|wav)$/.test(href)) {
return (
<figure>
@ -305,8 +309,8 @@ function _MarkDownContent(props: { content: string }) {
</video>
);
}
const isInternal = /^\/#/i.test(href);
const target = isInternal ? "_self" : aProps.target ?? "_blank";
const isInternal = /^\/#/.test(href);
const target = isInternal ? '_self' : aProps.target ?? '_blank';
return <a {...aProps} target={target} />;
},
}}
@ -335,18 +339,20 @@ export function Markdown(
className="markdown-body"
style={{
fontSize: `${props.fontSize ?? 14}px`,
fontFamily: props.fontFamily || "inherit",
fontFamily: props.fontFamily || 'inherit',
}}
ref={mdRef}
onContextMenu={props.onContextMenu}
onDoubleClickCapture={props.onDoubleClickCapture}
dir="auto"
>
{props.loading ? (
<LoadingIcon />
) : (
<MarkdownContent content={props.content} />
)}
{props.loading
? (
<LoadingIcon />
)
: (
<MarkdownContent content={props.content} />
)}
</div>
);
}

View File

@ -1,28 +1,59 @@
import { IconButton } from "./button";
import { ErrorBoundary } from "./error";
import type {
OnDragEndResponder,
} from '@hello-pangea/dnd';
import type { MultimodalContent } from '../client/api';
import type { Lang } from '../locales';
import styles from "./mask.module.scss";
import DownloadIcon from "../icons/download.svg";
import UploadIcon from "../icons/upload.svg";
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 CopyIcon from "../icons/copy.svg";
import DragIcon from "../icons/drag.svg";
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
import {
import type {
ChatMessage,
createMessage,
ModelConfig,
ModelType,
} from '../store';
import type { Mask } from '../store/mask';
import type { Updater } from '../typing';
import {
DragDropContext,
Draggable,
Droppable,
} from '@hello-pangea/dnd';
import clsx from 'clsx';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ROLES } from '../client/api';
import { FileName, Path } from '../constant';
import AddIcon from '../icons/add.svg';
import CloseIcon from '../icons/close.svg';
import CopyIcon from '../icons/copy.svg';
import DeleteIcon from '../icons/delete.svg';
import DownloadIcon from '../icons/download.svg';
import DragIcon from '../icons/drag.svg';
import EditIcon from '../icons/edit.svg';
import EyeIcon from '../icons/eye.svg';
import UploadIcon from '../icons/upload.svg';
import Locale, { ALL_LANG_OPTIONS, AllLangs } from '../locales';
import { BUILTIN_MASK_STORE } from '../masks';
import {
createMessage,
useAppConfig,
useChatStore,
} from "../store";
import { MultimodalContent, ROLES } from "../client/api";
} from '../store';
import { DEFAULT_MASK_AVATAR, useMaskStore } from '../store/mask';
import {
copyToClipboard,
downloadAs,
getMessageImages,
getMessageTextContent,
readFromFile,
} from '../utils';
import { IconButton } from './button';
import chatStyle from './chat.module.scss';
import { Avatar, AvatarPicker } from './emoji';
import { ErrorBoundary } from './error';
import styles from './mask.module.scss';
import { ModelConfigList } from './model-config';
import {
Input,
List,
@ -31,31 +62,7 @@ import {
Popover,
Select,
showConfirm,
} from "./ui-lib";
import { Avatar, AvatarPicker } from "./emoji";
import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
import { useNavigate } from "react-router-dom";
import chatStyle from "./chat.module.scss";
import { useState } from "react";
import {
copyToClipboard,
downloadAs,
getMessageImages,
readFromFile,
} from "../utils";
import { Updater } from "../typing";
import { ModelConfigList } from "./model-config";
import { FileName, Path } from "../constant";
import { BUILTIN_MASK_STORE } from "../masks";
import {
DragDropContext,
Droppable,
Draggable,
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { getMessageTextContent } from "../utils";
import clsx from "clsx";
} from './ui-lib';
// drag and drop helper function
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
@ -66,11 +73,13 @@ function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
}
export function MaskAvatar(props: { avatar: string; model?: ModelType }) {
return props.avatar !== DEFAULT_MASK_AVATAR ? (
<Avatar avatar={props.avatar} />
) : (
<Avatar model={props.model} />
);
return props.avatar !== DEFAULT_MASK_AVATAR
? (
<Avatar avatar={props.avatar} />
)
: (
<Avatar model={props.model} />
);
}
export function MaskConfig(props: {
@ -83,7 +92,8 @@ export function MaskConfig(props: {
const [showPicker, setShowPicker] = useState(false);
const updateConfig = (updater: (config: ModelConfig) => void) => {
if (props.readonly) return;
if (props.readonly)
{ return; }
const config = { ...props.mask.modelConfig };
updater(config);
@ -108,21 +118,22 @@ export function MaskConfig(props: {
updateContext={(updater) => {
const context = props.mask.context.slice();
updater(context);
props.updateMask((mask) => (mask.context = context));
props.updateMask(mask => (mask.context = context));
}}
/>
<List>
<ListItem title={Locale.Mask.Config.Avatar}>
<Popover
content={
content={(
<AvatarPicker
onEmojiClick={(emoji) => {
props.updateMask((mask) => (mask.avatar = emoji));
props.updateMask(mask => (mask.avatar = emoji));
setShowPicker(false);
}}
></AvatarPicker>
}
>
</AvatarPicker>
)}
open={showPicker}
onClose={() => setShowPicker(false)}
>
@ -130,7 +141,7 @@ export function MaskConfig(props: {
tabIndex={0}
aria-label={Locale.Mask.Config.Avatar}
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
style={{ cursor: 'pointer' }}
>
<MaskAvatar
avatar={props.mask.avatar}
@ -144,12 +155,12 @@ export function MaskConfig(props: {
aria-label={Locale.Mask.Config.Name}
type="text"
value={props.mask.name}
onInput={(e) =>
onInput={e =>
props.updateMask((mask) => {
mask.name = e.currentTarget.value;
})
}
></input>
})}
>
</input>
</ListItem>
<ListItem
title={Locale.Mask.Config.HideContext.Title}
@ -164,7 +175,8 @@ export function MaskConfig(props: {
mask.hideContext = e.currentTarget.checked;
});
}}
></input>
>
</input>
</ListItem>
{globalConfig.enableArtifacts && (
@ -181,7 +193,8 @@ export function MaskConfig(props: {
mask.enableArtifacts = e.currentTarget.checked;
});
}}
></input>
>
</input>
</ListItem>
)}
{globalConfig.enableCodeFold && (
@ -198,52 +211,58 @@ export function MaskConfig(props: {
mask.enableCodeFold = e.currentTarget.checked;
});
}}
></input>
>
</input>
</ListItem>
)}
{!props.shouldSyncFromGlobal ? (
<ListItem
title={Locale.Mask.Config.Share.Title}
subTitle={Locale.Mask.Config.Share.SubTitle}
>
<IconButton
aria={Locale.Mask.Config.Share.Title}
icon={<CopyIcon />}
text={Locale.Mask.Config.Share.Action}
onClick={copyMaskLink}
/>
</ListItem>
) : null}
{!props.shouldSyncFromGlobal
? (
<ListItem
title={Locale.Mask.Config.Share.Title}
subTitle={Locale.Mask.Config.Share.SubTitle}
>
<IconButton
aria={Locale.Mask.Config.Share.Title}
icon={<CopyIcon />}
text={Locale.Mask.Config.Share.Action}
onClick={copyMaskLink}
/>
</ListItem>
)
: null}
{props.shouldSyncFromGlobal ? (
<ListItem
title={Locale.Mask.Config.Sync.Title}
subTitle={Locale.Mask.Config.Sync.SubTitle}
>
<input
aria-label={Locale.Mask.Config.Sync.Title}
type="checkbox"
checked={props.mask.syncGlobalConfig}
onChange={async (e) => {
const checked = e.currentTarget.checked;
if (
checked &&
(await showConfirm(Locale.Mask.Config.Sync.Confirm))
) {
props.updateMask((mask) => {
mask.syncGlobalConfig = checked;
mask.modelConfig = { ...globalConfig.modelConfig };
});
} else if (!checked) {
props.updateMask((mask) => {
mask.syncGlobalConfig = checked;
});
}
}}
></input>
</ListItem>
) : null}
{props.shouldSyncFromGlobal
? (
<ListItem
title={Locale.Mask.Config.Sync.Title}
subTitle={Locale.Mask.Config.Sync.SubTitle}
>
<input
aria-label={Locale.Mask.Config.Sync.Title}
type="checkbox"
checked={props.mask.syncGlobalConfig}
onChange={async (e) => {
const checked = e.currentTarget.checked;
if (
checked
&& (await showConfirm(Locale.Mask.Config.Sync.Confirm))
) {
props.updateMask((mask) => {
mask.syncGlobalConfig = checked;
mask.modelConfig = { ...globalConfig.modelConfig };
});
} else if (!checked) {
props.updateMask((mask) => {
mask.syncGlobalConfig = checked;
});
}
}}
>
</input>
</ListItem>
)
: null}
</List>
<List>
@ -266,23 +285,22 @@ function ContextPromptItem(props: {
const [focusingInput, setFocusingInput] = useState(false);
return (
<div className={chatStyle["context-prompt-row"]}>
<div className={chatStyle['context-prompt-row']}>
{!focusingInput && (
<>
<div className={chatStyle["context-drag"]}>
<div className={chatStyle['context-drag']}>
<DragIcon />
</div>
<Select
value={props.prompt.role}
className={chatStyle["context-role"]}
onChange={(e) =>
className={chatStyle['context-role']}
onChange={e =>
props.update({
...props.prompt,
role: e.target.value as any,
})
}
})}
>
{ROLES.map((r) => (
{ROLES.map(r => (
<option key={r} value={r}>
{r}
</option>
@ -293,7 +311,7 @@ function ContextPromptItem(props: {
<Input
value={getMessageTextContent(props.prompt)}
type="text"
className={chatStyle["context-content"]}
className={chatStyle['context-content']}
rows={focusingInput ? 5 : 1}
onFocus={() => setFocusingInput(true)}
onBlur={() => {
@ -302,17 +320,16 @@ function ContextPromptItem(props: {
// extensions like "Translate" will always display a floating bar
window?.getSelection()?.removeAllRanges();
}}
onInput={(e) =>
onInput={e =>
props.update({
...props.prompt,
content: e.currentTarget.value as any,
})
}
})}
/>
{!focusingInput && (
<IconButton
icon={<DeleteIcon />}
className={chatStyle["context-delete-button"]}
className={chatStyle['context-delete-button']}
onClick={() => props.remove()}
bordered
/>
@ -328,11 +345,11 @@ export function ContextPrompts(props: {
const context = props.context;
const addContextPrompt = (prompt: ChatMessage, i: number) => {
props.updateContext((context) => context.splice(i, 0, prompt));
props.updateContext(context => context.splice(i, 0, prompt));
};
const removeContextPrompt = (i: number) => {
props.updateContext((context) => context.splice(i, 1));
props.updateContext(context => context.splice(i, 1));
};
const updateContextPrompt = (i: number, prompt: ChatMessage) => {
@ -341,9 +358,9 @@ export function ContextPrompts(props: {
context[i] = prompt;
if (images.length > 0) {
const text = getMessageTextContent(context[i]);
const newContext: MultimodalContent[] = [{ type: "text", text }];
const newContext: MultimodalContent[] = [{ type: 'text', text }];
for (const img of images) {
newContext.push({ type: "image_url", image_url: { url: img } });
newContext.push({ type: 'image_url', image_url: { url: img } });
}
context[i].content = newContext;
}
@ -366,10 +383,10 @@ export function ContextPrompts(props: {
return (
<>
<div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
<div className={chatStyle['context-prompt']} style={{ marginBottom: 20 }}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="context-prompt-list">
{(provided) => (
{provided => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{context.map((c, i) => (
<Draggable
@ -377,7 +394,7 @@ export function ContextPrompts(props: {
index={i}
key={c.id}
>
{(provided) => (
{provided => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
@ -386,16 +403,16 @@ export function ContextPrompts(props: {
<ContextPromptItem
index={i}
prompt={c}
update={(prompt) => updateContextPrompt(i, prompt)}
update={prompt => updateContextPrompt(i, prompt)}
remove={() => removeContextPrompt(i)}
/>
<div
className={chatStyle["context-prompt-insert"]}
className={chatStyle['context-prompt-insert']}
onClick={() => {
addContextPrompt(
createMessage({
role: "user",
content: "",
role: 'user',
content: '',
date: new Date().toLocaleString(),
}),
i + 1,
@ -415,22 +432,21 @@ export function ContextPrompts(props: {
</DragDropContext>
{props.context.length === 0 && (
<div className={chatStyle["context-prompt-row"]}>
<div className={chatStyle['context-prompt-row']}>
<IconButton
icon={<AddIcon />}
text={Locale.Context.Add}
bordered
className={chatStyle["context-prompt-button"]}
className={chatStyle['context-prompt-button']}
onClick={() =>
addContextPrompt(
createMessage({
role: "user",
content: "",
date: "",
role: 'user',
content: '',
date: '',
}),
props.context.length,
)
}
)}
/>
</div>
)}
@ -449,17 +465,17 @@ export function MaskPage() {
const allMasks = maskStore
.getAll()
.filter((m) => !filterLang || m.lang === filterLang);
.filter(m => !filterLang || m.lang === filterLang);
const [searchMasks, setSearchMasks] = useState<Mask[]>([]);
const [searchText, setSearchText] = useState("");
const [searchText, setSearchText] = useState('');
const masks = searchText.length > 0 ? searchMasks : allMasks;
// refactored already, now it accurate
const onSearch = (text: string) => {
setSearchText(text);
if (text.length > 0) {
const result = allMasks.filter((m) =>
const result = allMasks.filter(m =>
m.name.toLowerCase().includes(text.toLowerCase()),
);
setSearchMasks(result);
@ -469,12 +485,12 @@ export function MaskPage() {
};
const [editingMaskId, setEditingMaskId] = useState<string | undefined>();
const editingMask =
maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
const editingMask
= maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
const closeMaskModal = () => setEditingMaskId(undefined);
const downloadAll = () => {
downloadAs(JSON.stringify(masks.filter((v) => !v.builtin)), FileName.Masks);
downloadAs(JSON.stringify(masks.filter(v => !v.builtin)), FileName.Masks);
};
const importFromFile = () => {
@ -489,7 +505,7 @@ export function MaskPage() {
}
return;
}
//if the content is a single mask.
// if the content is a single mask.
if (importMasks.name) {
maskStore.create(importMasks);
}
@ -499,7 +515,7 @@ export function MaskPage() {
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
<div className={styles['mask-page']}>
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
@ -537,17 +553,17 @@ export function MaskPage() {
</div>
</div>
<div className={styles["mask-page-body"]}>
<div className={styles["mask-filter"]}>
<div className={styles['mask-page-body']}>
<div className={styles['mask-filter']}>
<input
type="text"
className={styles["search-bar"]}
className={styles['search-bar']}
placeholder={Locale.Mask.Page.Search}
autoFocus
onInput={(e) => onSearch(e.currentTarget.value)}
onInput={e => onSearch(e.currentTarget.value)}
/>
<Select
className={styles["mask-filter-lang"]}
className={styles['mask-filter-lang']}
value={filterLang ?? Locale.Settings.Lang.All}
onChange={(e) => {
const value = e.currentTarget.value;
@ -561,7 +577,7 @@ export function MaskPage() {
<option key="all" value={Locale.Settings.Lang.All}>
{Locale.Settings.Lang.All}
</option>
{AllLangs.map((lang) => (
{AllLangs.map(lang => (
<option value={lang} key={lang}>
{ALL_LANG_OPTIONS[lang]}
</option>
@ -569,7 +585,7 @@ export function MaskPage() {
</Select>
<IconButton
className={styles["mask-create"]}
className={styles['mask-create']}
icon={<AddIcon />}
text={Locale.Mask.Page.Create}
bordered
@ -581,22 +597,22 @@ export function MaskPage() {
</div>
<div>
{masks.map((m) => (
<div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}>
<div className={styles["mask-icon"]}>
{masks.map(m => (
<div className={styles['mask-item']} key={m.id}>
<div className={styles['mask-header']}>
<div className={styles['mask-icon']}>
<MaskAvatar avatar={m.avatar} model={m.modelConfig.model} />
</div>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{m.name}</div>
<div className={clsx(styles["mask-info"], "one-line")}>
<div className={styles['mask-title']}>
<div className={styles['mask-name']}>{m.name}</div>
<div className={clsx(styles['mask-info'], 'one-line')}>
{`${Locale.Mask.Item.Info(m.context.length)} / ${
ALL_LANG_OPTIONS[m.lang]
} / ${m.modelConfig.model}`}
</div>
</div>
</div>
<div className={styles["mask-actions"]}>
<div className={styles['mask-actions']}>
<IconButton
icon={<AddIcon />}
text={Locale.Mask.Item.Chat}
@ -605,19 +621,21 @@ export function MaskPage() {
navigate(Path.Chat);
}}
/>
{m.builtin ? (
<IconButton
icon={<EyeIcon />}
text={Locale.Mask.Item.View}
onClick={() => setEditingMaskId(m.id)}
/>
) : (
<IconButton
icon={<EditIcon />}
text={Locale.Mask.Item.Edit}
onClick={() => setEditingMaskId(m.id)}
/>
)}
{m.builtin
? (
<IconButton
icon={<EyeIcon />}
text={Locale.Mask.Item.View}
onClick={() => setEditingMaskId(m.id)}
/>
)
: (
<IconButton
icon={<EditIcon />}
text={Locale.Mask.Item.Edit}
onClick={() => setEditingMaskId(m.id)}
/>
)}
{!m.builtin && (
<IconButton
icon={<DeleteIcon />}
@ -651,8 +669,7 @@ export function MaskPage() {
downloadAs(
JSON.stringify(editingMask),
`${editingMask.name}.json`,
)
}
)}
/>,
<IconButton
key="copy"
@ -669,9 +686,8 @@ export function MaskPage() {
>
<MaskConfig
mask={editingMask}
updateMask={(updater) =>
maskStore.updateMask(editingMaskId!, updater)
}
updateMask={updater =>
maskStore.updateMask(editingMaskId!, updater)}
readonly={editingMask.builtin}
/>
</Modal>

View File

@ -1,14 +1,15 @@
import { useEffect, useMemo, useState } from "react";
import { ChatMessage, useAppConfig, useChatStore } from "../store";
import { Updater } from "../typing";
import { IconButton } from "./button";
import { Avatar } from "./emoji";
import { MaskAvatar } from "./mask";
import Locale from "../locales";
import type { ChatMessage } from '../store';
import type { Updater } from '../typing';
import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react';
import Locale from '../locales';
import { useAppConfig, useChatStore } from '../store';
import { getMessageTextContent } from '../utils';
import { IconButton } from './button';
import styles from "./message-selector.module.scss";
import { getMessageTextContent } from "../utils";
import clsx from "clsx";
import { Avatar } from './emoji';
import { MaskAvatar } from './mask';
import styles from './message-selector.module.scss';
function useShiftRange() {
const [startIndex, setStartIndex] = useState<number>();
@ -26,22 +27,24 @@ function useShiftRange() {
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key !== "Shift") return;
if (e.key !== 'Shift')
{ return; }
setShiftDown(true);
};
const onKeyUp = (e: KeyboardEvent) => {
if (e.key !== "Shift") return;
if (e.key !== 'Shift')
{ return; }
setShiftDown(false);
setStartIndex(undefined);
setEndIndex(undefined);
};
window.addEventListener("keyup", onKeyUp);
window.addEventListener("keydown", onKeyDown);
window.addEventListener('keyup', onKeyUp);
window.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener("keyup", onKeyUp);
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('keydown', onKeyDown);
};
}, []);
@ -88,16 +91,16 @@ export function MessageSelector(props: {
() =>
allMessages.filter(
(m, i) =>
m.id && // message must have id
isValid(m) &&
(i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
m.id // message must have id
&& isValid(m)
&& (i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
),
[allMessages],
);
const messageCount = messages.length;
const config = useAppConfig();
const [searchInput, setSearchInput] = useState("");
const [searchInput, setSearchInput] = useState('');
const [searchIds, setSearchIds] = useState(new Set<string>());
const isInSearchResult = (id: string) => {
return searchInput.length === 0 || searchIds.has(id);
@ -105,7 +108,7 @@ export function MessageSelector(props: {
const doSearch = (text: string) => {
const searchResults = new Set<string>();
if (text.length > 0) {
messages.forEach((m) =>
messages.forEach(m =>
getMessageTextContent(m).includes(text)
? searchResults.add(m.id!)
: null,
@ -118,8 +121,8 @@ export function MessageSelector(props: {
const { startIndex, endIndex, onClickIndex } = useShiftRange();
const selectAll = () => {
props.updateSelection((selection) =>
messages.forEach((m) => selection.add(m.id!)),
props.updateSelection(selection =>
messages.forEach(m => selection.add(m.id!)),
);
};
@ -144,60 +147,60 @@ export function MessageSelector(props: {
}, [startIndex, endIndex]);
return (
<div className={styles["message-selector"]}>
<div className={styles["message-filter"]}>
<div className={styles['message-selector']}>
<div className={styles['message-filter']}>
<input
type="text"
placeholder={Locale.Select.Search}
className={clsx(styles["filter-item"], styles["search-bar"])}
className={clsx(styles['filter-item'], styles['search-bar'])}
value={searchInput}
onInput={(e) => {
setSearchInput(e.currentTarget.value);
doSearch(e.currentTarget.value);
}}
></input>
>
</input>
<div className={styles["actions"]}>
<div className={styles.actions}>
<IconButton
text={Locale.Select.All}
bordered
className={styles["filter-item"]}
className={styles['filter-item']}
onClick={selectAll}
/>
<IconButton
text={Locale.Select.Latest}
bordered
className={styles["filter-item"]}
className={styles['filter-item']}
onClick={() =>
props.updateSelection((selection) => {
selection.clear();
messages
.slice(messageCount - LATEST_COUNT)
.forEach((m) => selection.add(m.id!));
})
}
.forEach(m => selection.add(m.id!));
})}
/>
<IconButton
text={Locale.Select.Clear}
bordered
className={styles["filter-item"]}
className={styles['filter-item']}
onClick={() =>
props.updateSelection((selection) => selection.clear())
}
props.updateSelection(selection => selection.clear())}
/>
</div>
</div>
<div className={styles["messages"]}>
<div className={styles.messages}>
{messages.map((m, i) => {
if (!isInSearchResult(m.id!)) return null;
if (!isInSearchResult(m.id!))
{ return null; }
const id = m.id ?? i;
const isSelected = props.selection.has(id);
return (
<div
className={clsx(styles["message"], {
[styles["message-selected"]]: props.selection.has(m.id!),
className={clsx(styles.message, {
[styles['message-selected']]: props.selection.has(m.id!),
})}
key={i}
onClick={() => {
@ -207,26 +210,28 @@ export function MessageSelector(props: {
onClickIndex(i);
}}
>
<div className={styles["avatar"]}>
{m.role === "user" ? (
<Avatar avatar={config.avatar}></Avatar>
) : (
<MaskAvatar
avatar={session.mask.avatar}
model={m.model || session.mask.modelConfig.model}
/>
)}
<div className={styles.avatar}>
{m.role === 'user'
? (
<Avatar avatar={config.avatar}></Avatar>
)
: (
<MaskAvatar
avatar={session.mask.avatar}
model={m.model || session.mask.modelConfig.model}
/>
)}
</div>
<div className={styles["body"]}>
<div className={styles["date"]}>
<div className={styles.body}>
<div className={styles.date}>
{new Date(m.date).toLocaleString()}
</div>
<div className={clsx(styles["content"], "one-line")}>
<div className={clsx(styles.content, 'one-line')}>
{getMessageTextContent(m)}
</div>
</div>
<div className={styles["checkbox"]}>
<div className={styles.checkbox}>
<input type="checkbox" checked={isSelected} readOnly></input>
</div>
</div>

View File

@ -1,13 +1,14 @@
import { ServiceProvider } from "@/app/constant";
import { ModalConfigValidator, ModelConfig } from "../store";
import type { ModelConfig } from '../store';
import { ServiceProvider } from '@/app/constant';
import Locale from "../locales";
import { InputRange } from "./input-range";
import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks";
import { groupBy } from "lodash-es";
import styles from "./model-config.module.scss";
import { getModelProvider } from "../utils/model";
import { groupBy } from 'lodash-es';
import Locale from '../locales';
import { ModalConfigValidator } from '../store';
import { useAllModels } from '../utils/hooks';
import { getModelProvider } from '../utils/model';
import { InputRange } from './input-range';
import styles from './model-config.module.scss';
import { ListItem, Select } from './ui-lib';
export function ModelConfigList(props: {
modelConfig: ModelConfig;
@ -15,8 +16,8 @@ export function ModelConfigList(props: {
}) {
const allModels = useAllModels();
const groupModels = groupBy(
allModels.filter((v) => v.available),
"provider.providerName",
allModels.filter(v => v.available),
'provider.providerName',
);
const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
@ -61,13 +62,14 @@ export function ModelConfigList(props: {
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
config =>
(config.temperature = ModalConfigValidator.temperature(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
>
</InputRange>
</ListItem>
<ListItem
title={Locale.Settings.TopP.Title}
@ -81,13 +83,14 @@ export function ModelConfigList(props: {
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
config =>
(config.top_p = ModalConfigValidator.top_p(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
>
</InputRange>
</ListItem>
<ListItem
title={Locale.Settings.MaxTokens.Title}
@ -99,98 +102,102 @@ export function ModelConfigList(props: {
min={1024}
max={512000}
value={props.modelConfig.max_tokens}
onChange={(e) =>
onChange={e =>
props.updateConfig(
(config) =>
config =>
(config.max_tokens = ModalConfigValidator.max_tokens(
e.currentTarget.valueAsNumber,
)),
)
}
></input>
)}
>
</input>
</ListItem>
{props.modelConfig?.providerName == ServiceProvider.Google ? null : (
<>
<ListItem
title={Locale.Settings.PresencePenalty.Title}
subTitle={Locale.Settings.PresencePenalty.SubTitle}
>
<InputRange
aria={Locale.Settings.PresencePenalty.Title}
value={props.modelConfig.presence_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.presence_penalty =
ModalConfigValidator.presence_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
{props.modelConfig?.providerName == ServiceProvider.Google
? null
: (
<>
<ListItem
title={Locale.Settings.PresencePenalty.Title}
subTitle={Locale.Settings.PresencePenalty.SubTitle}
>
<InputRange
aria={Locale.Settings.PresencePenalty.Title}
value={props.modelConfig.presence_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
config =>
(config.presence_penalty
= ModalConfigValidator.presence_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
>
</InputRange>
</ListItem>
<ListItem
title={Locale.Settings.FrequencyPenalty.Title}
subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
>
<InputRange
aria={Locale.Settings.FrequencyPenalty.Title}
value={props.modelConfig.frequency_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.frequency_penalty =
ModalConfigValidator.frequency_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.FrequencyPenalty.Title}
subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
>
<InputRange
aria={Locale.Settings.FrequencyPenalty.Title}
value={props.modelConfig.frequency_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
config =>
(config.frequency_penalty
= ModalConfigValidator.frequency_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
>
</InputRange>
</ListItem>
<ListItem
title={Locale.Settings.InjectSystemPrompts.Title}
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
>
<input
aria-label={Locale.Settings.InjectSystemPrompts.Title}
type="checkbox"
checked={props.modelConfig.enableInjectSystemPrompts}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.enableInjectSystemPrompts =
e.currentTarget.checked),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.InjectSystemPrompts.Title}
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
>
<input
aria-label={Locale.Settings.InjectSystemPrompts.Title}
type="checkbox"
checked={props.modelConfig.enableInjectSystemPrompts}
onChange={e =>
props.updateConfig(
config =>
(config.enableInjectSystemPrompts
= e.currentTarget.checked),
)}
>
</input>
</ListItem>
<ListItem
title={Locale.Settings.InputTemplate.Title}
subTitle={Locale.Settings.InputTemplate.SubTitle}
>
<input
aria-label={Locale.Settings.InputTemplate.Title}
type="text"
value={props.modelConfig.template}
onChange={(e) =>
props.updateConfig(
(config) => (config.template = e.currentTarget.value),
)
}
></input>
</ListItem>
</>
)}
<ListItem
title={Locale.Settings.InputTemplate.Title}
subTitle={Locale.Settings.InputTemplate.SubTitle}
>
<input
aria-label={Locale.Settings.InputTemplate.Title}
type="text"
value={props.modelConfig.template}
onChange={e =>
props.updateConfig(
config => (config.template = e.currentTarget.value),
)}
>
</input>
</ListItem>
</>
)}
<ListItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
@ -202,12 +209,12 @@ export function ModelConfigList(props: {
min="0"
max="64"
step="1"
onChange={(e) =>
onChange={e =>
props.updateConfig(
(config) => (config.historyMessageCount = e.target.valueAsNumber),
)
}
></InputRange>
config => (config.historyMessageCount = e.target.valueAsNumber),
)}
>
</InputRange>
</ListItem>
<ListItem
@ -220,33 +227,33 @@ export function ModelConfigList(props: {
min={500}
max={4000}
value={props.modelConfig.compressMessageLengthThreshold}
onChange={(e) =>
onChange={e =>
props.updateConfig(
(config) =>
(config.compressMessageLengthThreshold =
e.currentTarget.valueAsNumber),
)
}
></input>
config =>
(config.compressMessageLengthThreshold
= e.currentTarget.valueAsNumber),
)}
>
</input>
</ListItem>
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
<input
aria-label={Locale.Memory.Title}
type="checkbox"
checked={props.modelConfig.sendMemory}
onChange={(e) =>
onChange={e =>
props.updateConfig(
(config) => (config.sendMemory = e.currentTarget.checked),
)
}
></input>
config => (config.sendMemory = e.currentTarget.checked),
)}
>
</input>
</ListItem>
<ListItem
title={Locale.Settings.CompressModel.Title}
subTitle={Locale.Settings.CompressModel.SubTitle}
>
<Select
className={styles["select-compress-model"]}
className={styles['select-compress-model']}
aria-label={Locale.Settings.CompressModel.Title}
value={compressModelValue}
onChange={(e) => {
@ -260,10 +267,13 @@ export function ModelConfigList(props: {
}}
>
{allModels
.filter((v) => v.available)
.filter(v => v.available)
.map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName})
{v.displayName}
(
{v.provider?.providerName}
)
</option>
))}
</Select>

View File

@ -72,12 +72,7 @@
align-items: center;
padding-top: 20px;
$linear: linear-gradient(
to bottom,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 1),
rgba(0, 0, 0, 0)
);
$linear: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1), rgba(0, 0, 0, 0));
-webkit-mask-image: $linear;
mask-image: $linear;

View File

@ -1,31 +1,32 @@
import { useEffect, useRef, useState } from "react";
import { Path, SlotID } from "../constant";
import { IconButton } from "./button";
import { EmojiAvatar } from "./emoji";
import styles from "./new-chat.module.scss";
import type { Mask } from '../store/mask';
import clsx from 'clsx';
import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useCommand } from '../command';
import LeftIcon from "../icons/left.svg";
import LightningIcon from "../icons/lightning.svg";
import EyeIcon from "../icons/eye.svg";
import { Path, SlotID } from '../constant';
import EyeIcon from '../icons/eye.svg';
import LeftIcon from '../icons/left.svg';
import { useLocation, useNavigate } from "react-router-dom";
import { Mask, useMaskStore } from "../store/mask";
import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store";
import { MaskAvatar } from "./mask";
import { useCommand } from "../command";
import { showConfirm } from "./ui-lib";
import { BUILTIN_MASK_STORE } from "../masks";
import clsx from "clsx";
import LightningIcon from '../icons/lightning.svg';
import Locale from '../locales';
import { BUILTIN_MASK_STORE } from '../masks';
import { useAppConfig, useChatStore } from '../store';
import { useMaskStore } from '../store/mask';
import { IconButton } from './button';
import { EmojiAvatar } from './emoji';
import { MaskAvatar } from './mask';
import styles from './new-chat.module.scss';
import { showConfirm } from './ui-lib';
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
return (
<div className={styles["mask"]} onClick={props.onClick}>
<div className={styles.mask} onClick={props.onClick}>
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
<div className={clsx(styles["mask-name"], "one-line")}>
<div className={clsx(styles['mask-name'], 'one-line')}>
{props.mask.name}
</div>
</div>
@ -38,7 +39,8 @@ function useMaskGroup(masks: Mask[]) {
useEffect(() => {
const computeGroup = () => {
const appBody = document.getElementById(SlotID.AppBody);
if (!appBody || masks.length === 0) return;
if (!appBody || masks.length === 0)
{ return; }
const rect = appBody.getBoundingClientRect();
const maxWidth = rect.width;
@ -66,8 +68,8 @@ function useMaskGroup(masks: Mask[]) {
computeGroup();
window.addEventListener("resize", computeGroup);
return () => window.removeEventListener("resize", computeGroup);
window.addEventListener('resize', computeGroup);
return () => window.removeEventListener('resize', computeGroup);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -101,26 +103,27 @@ export function NewChat() {
const mask = maskStore.get(id) ?? BUILTIN_MASK_STORE.get(id);
startChat(mask ?? undefined);
} catch {
console.error("[New Chat] failed to create chat from mask id=", id);
console.error('[New Chat] failed to create chat from mask id=', id);
}
},
});
useEffect(() => {
if (maskRef.current) {
maskRef.current.scrollLeft =
(maskRef.current.scrollWidth - maskRef.current.clientWidth) / 2;
maskRef.current.scrollLeft
= (maskRef.current.scrollWidth - maskRef.current.clientWidth) / 2;
}
}, [groups]);
return (
<div className={styles["new-chat"]}>
<div className={styles["mask-header"]}>
<div className={styles['new-chat']}>
<div className={styles['mask-header']}>
<IconButton
icon={<LeftIcon />}
text={Locale.NewChat.Return}
onClick={() => navigate(Path.Home)}
></IconButton>
>
</IconButton>
{!state?.fromHome && (
<IconButton
text={Locale.NewChat.NotShow}
@ -128,29 +131,30 @@ export function NewChat() {
if (await showConfirm(Locale.NewChat.ConfirmNoShow)) {
startChat();
config.update(
(config) => (config.dontShowMaskSplashScreen = true),
config => (config.dontShowMaskSplashScreen = true),
);
}
}}
></IconButton>
>
</IconButton>
)}
</div>
<div className={styles["mask-cards"]}>
<div className={styles["mask-card"]}>
<div className={styles['mask-cards']}>
<div className={styles['mask-card']}>
<EmojiAvatar avatar="1f606" size={24} />
</div>
<div className={styles["mask-card"]}>
<div className={styles['mask-card']}>
<EmojiAvatar avatar="1f916" size={24} />
</div>
<div className={styles["mask-card"]}>
<div className={styles['mask-card']}>
<EmojiAvatar avatar="1f479" size={24} />
</div>
</div>
<div className={styles["title"]}>{Locale.NewChat.Title}</div>
<div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
<div className={styles.title}>{Locale.NewChat.Title}</div>
<div className={styles['sub-title']}>{Locale.NewChat.SubTitle}</div>
<div className={styles["actions"]}>
<div className={styles.actions}>
<IconButton
text={Locale.NewChat.More}
onClick={() => navigate(Path.Masks)}
@ -165,13 +169,13 @@ export function NewChat() {
icon={<LightningIcon />}
type="primary"
shadow
className={styles["skip"]}
className={styles.skip}
/>
</div>
<div className={styles["masks"]} ref={maskRef}>
<div className={styles.masks} ref={maskRef}>
{groups.map((masks, i) => (
<div key={i} className={styles["mask-row"]}>
<div key={i} className={styles['mask-row']}>
{masks.map((mask, index) => (
<MaskItem
key={index}

View File

@ -23,8 +23,8 @@
margin-right: 20px;
@media screen and (max-width: 600px) {
margin-right: 0px;
}
margin-right: 0px;
}
}
@media screen and (max-width: 600px) {

View File

@ -1,34 +1,35 @@
import { useDebouncedCallback } from "use-debounce";
import OpenAPIClientAxios from "openapi-client-axios";
import yaml from "js-yaml";
import { PLUGINS_REPO_URL } from "../constant";
import { IconButton } from "./button";
import { ErrorBoundary } from "./error";
import type { Plugin } from '../store/plugin';
import clsx from 'clsx';
import yaml from 'js-yaml';
import OpenAPIClientAxios from 'openapi-client-axios';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import styles from "./mask.module.scss";
import pluginStyles from "./plugin.module.scss";
import { useDebouncedCallback } from 'use-debounce';
import { PLUGINS_REPO_URL } from '../constant';
import EditIcon from "../icons/edit.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import ConfirmIcon from "../icons/confirm.svg";
import ReloadIcon from "../icons/reload.svg";
import GithubIcon from "../icons/github.svg";
import AddIcon from '../icons/add.svg';
import CloseIcon from '../icons/close.svg';
import ConfirmIcon from '../icons/confirm.svg';
import DeleteIcon from '../icons/delete.svg';
import EditIcon from '../icons/edit.svg';
import GithubIcon from '../icons/github.svg';
import ReloadIcon from '../icons/reload.svg';
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
import Locale from '../locales';
import { FunctionToolService, usePluginStore } from '../store/plugin';
import { IconButton } from './button';
import { ErrorBoundary } from './error';
import styles from './mask.module.scss';
import pluginStyles from './plugin.module.scss';
import {
PasswordInput,
List,
ListItem,
Modal,
PasswordInput,
showConfirm,
showToast,
} from "./ui-lib";
import Locale from "../locales";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import clsx from "clsx";
} from './ui-lib';
export function PluginPage() {
const navigate = useNavigate();
@ -36,7 +37,7 @@ export function PluginPage() {
const allPlugins = pluginStore.getAll();
const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
const [searchText, setSearchText] = useState("");
const [searchText, setSearchText] = useState('');
const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
// refactored already, now it accurate
@ -44,7 +45,7 @@ export function PluginPage() {
setSearchText(text);
if (text.length > 0) {
const result = allPlugins.filter(
(m) => m?.title.toLowerCase().includes(text.toLowerCase()),
m => m?.title.toLowerCase().includes(text.toLowerCase()),
);
setSearchPlugins(result);
} else {
@ -85,21 +86,21 @@ export function PluginPage() {
}
}, 100).bind(null, editingPlugin);
const [loadUrl, setLoadUrl] = useState<string>("");
const [loadUrl, setLoadUrl] = useState<string>('');
const loadFromUrl = (loadUrl: string) =>
fetch(loadUrl)
.catch((e) => {
const p = new URL(loadUrl);
return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
headers: {
"X-Base-URL": p.origin,
'X-Base-URL': p.origin,
},
});
})
.then((res) => res.text())
.then(res => res.text())
.then((content) => {
try {
return JSON.stringify(JSON.parse(content), null, " ");
return JSON.stringify(JSON.parse(content), null, ' ');
} catch (e) {
return content;
}
@ -118,7 +119,7 @@ export function PluginPage() {
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
<div className={styles['mask-page']}>
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
@ -149,18 +150,18 @@ export function PluginPage() {
</div>
</div>
<div className={styles["mask-page-body"]}>
<div className={styles["mask-filter"]}>
<div className={styles['mask-page-body']}>
<div className={styles['mask-filter']}>
<input
type="text"
className={styles["search-bar"]}
className={styles['search-bar']}
placeholder={Locale.Plugin.Page.Search}
autoFocus
onInput={(e) => onSearch(e.currentTarget.value)}
onInput={e => onSearch(e.currentTarget.value)}
/>
<IconButton
className={styles["mask-create"]}
className={styles['mask-create']}
icon={<AddIcon />}
text={Locale.Plugin.Page.Create}
bordered
@ -175,10 +176,10 @@ export function PluginPage() {
{plugins.length == 0 && (
<div
style={{
display: "flex",
margin: "60px auto",
alignItems: "center",
justifyContent: "center",
display: 'flex',
margin: '60px auto',
alignItems: 'center',
justifyContent: 'center',
}}
>
{Locale.Plugin.Page.Find}
@ -192,22 +193,24 @@ export function PluginPage() {
</a>
</div>
)}
{plugins.map((m) => (
<div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}>
<div className={styles["mask-icon"]}></div>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>
{m.title}@<small>{m.version}</small>
{plugins.map(m => (
<div className={styles['mask-item']} key={m.id}>
<div className={styles['mask-header']}>
<div className={styles['mask-icon']}></div>
<div className={styles['mask-title']}>
<div className={styles['mask-name']}>
{m.title}
@
<small>{m.version}</small>
</div>
<div className={clsx(styles["mask-info"], "one-line")}>
<div className={clsx(styles['mask-info'], 'one-line')}>
{Locale.Plugin.Item.Info(
FunctionToolService.add(m).length,
)}
</div>
</div>
</div>
<div className={styles["mask-actions"]}>
<div className={styles['mask-actions']}>
<IconButton
icon={<EditIcon />}
text={Locale.Plugin.Item.Edit}
@ -244,7 +247,7 @@ export function PluginPage() {
text={Locale.UI.Confirm}
key="export"
bordered
onClick={() => setEditingPluginId("")}
onClick={() => setEditingPluginId('')}
/>,
]}
>
@ -264,7 +267,7 @@ export function PluginPage() {
<option value="custom">{Locale.Plugin.Auth.Custom}</option>
</select>
</ListItem>
{["bearer", "basic", "custom"].includes(
{['bearer', 'basic', 'custom'].includes(
editingPlugin.authType as string,
) && (
<ListItem title={Locale.Plugin.Auth.Location}>
@ -288,7 +291,7 @@ export function PluginPage() {
</select>
</ListItem>
)}
{editingPlugin.authType == "custom" && (
{editingPlugin.authType == 'custom' && (
<ListItem title={Locale.Plugin.Auth.CustomHeader}>
<input
type="text"
@ -298,10 +301,11 @@ export function PluginPage() {
plugin.authHeader = e.target.value;
});
}}
></input>
>
</input>
</ListItem>
)}
{["bearer", "basic", "custom"].includes(
{['bearer', 'basic', 'custom'].includes(
editingPlugin.authType as string,
) && (
<ListItem title={Locale.Plugin.Auth.Token}>
@ -313,18 +317,20 @@ export function PluginPage() {
plugin.authToken = e.currentTarget.value;
});
}}
></PasswordInput>
>
</PasswordInput>
</ListItem>
)}
</List>
<List>
<ListItem title={Locale.Plugin.EditModal.Content}>
<div className={pluginStyles["plugin-schema"]}>
<div className={pluginStyles['plugin-schema']}>
<input
type="text"
style={{ minWidth: 200 }}
onInput={(e) => setLoadUrl(e.currentTarget.value)}
></input>
onInput={e => setLoadUrl(e.currentTarget.value)}
>
</input>
<IconButton
icon={<ReloadIcon />}
text={Locale.Plugin.EditModal.Load}
@ -334,26 +340,28 @@ export function PluginPage() {
</div>
</ListItem>
<ListItem
subTitle={
subTitle={(
<div
className={clsx(
"markdown-body",
pluginStyles["plugin-content"],
'markdown-body',
pluginStyles['plugin-content'],
)}
dir="auto"
>
<pre>
<code
contentEditable={true}
contentEditable
dangerouslySetInnerHTML={{
__html: editingPlugin.content,
}}
onBlur={onChangePlugin}
></code>
>
</code>
</pre>
</div>
}
></ListItem>
)}
>
</ListItem>
{editingPluginTool?.tools.map((tool, index) => (
<ListItem
key={index}

View File

@ -1 +1 @@
export * from "./realtime-chat";
export * from './realtime-chat';

View File

@ -1,26 +1,28 @@
import VoiceIcon from "@/app/icons/voice.svg";
import VoiceOffIcon from "@/app/icons/voice-off.svg";
import PowerIcon from "@/app/icons/power.svg";
import styles from "./realtime-chat.module.scss";
import clsx from "clsx";
import { useState, useRef, useEffect } from "react";
import { useChatStore, createMessage, useAppConfig } from "@/app/store";
import { IconButton } from "@/app/components/button";
import {
import type {
Modality,
RTClient,
RTInputAudioItem,
RTResponse,
TurnDetection,
} from "rt-client";
import { AudioHandler } from "@/app/lib/audio";
import { uploadImage } from "@/app/utils/chat";
import { VoicePrint } from "@/app/components/voice-print";
} from 'rt-client';
import { IconButton } from '@/app/components/button';
import { VoicePrint } from '@/app/components/voice-print';
import PowerIcon from '@/app/icons/power.svg';
import VoiceIcon from '@/app/icons/voice.svg';
import VoiceOffIcon from '@/app/icons/voice-off.svg';
import { AudioHandler } from '@/app/lib/audio';
import { createMessage, useAppConfig, useChatStore } from '@/app/store';
import { uploadImage } from '@/app/utils/chat';
import clsx from 'clsx';
import { useEffect, useRef, useState } from 'react';
import {
RTClient,
} from 'rt-client';
import styles from './realtime-chat.module.scss';
interface RealtimeChatProps {
onClose?: () => void;
@ -36,11 +38,11 @@ export function RealtimeChat({
const chatStore = useChatStore();
const session = chatStore.currentSession();
const config = useAppConfig();
const [status, setStatus] = useState("");
const [status, setStatus] = useState('');
const [isRecording, setIsRecording] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [modality, setModality] = useState("audio");
const [modality, setModality] = useState('audio');
const [useVAD, setUseVAD] = useState(true);
const [frequencies, setFrequencies] = useState<Uint8Array | undefined>();
@ -51,32 +53,33 @@ export function RealtimeChat({
const temperature = config.realtimeConfig.temperature;
const apiKey = config.realtimeConfig.apiKey;
const model = config.realtimeConfig.model;
const azure = config.realtimeConfig.provider === "Azure";
const azure = config.realtimeConfig.provider === 'Azure';
const azureEndpoint = config.realtimeConfig.azure.endpoint;
const azureDeployment = config.realtimeConfig.azure.deployment;
const voice = config.realtimeConfig.voice;
const handleConnect = async () => {
if (isConnecting) return;
if (isConnecting)
{ return; }
if (!isConnected) {
try {
setIsConnecting(true);
clientRef.current = azure
? new RTClient(
new URL(azureEndpoint),
{ key: apiKey },
{ deployment: azureDeployment },
)
new URL(azureEndpoint),
{ key: apiKey },
{ deployment: azureDeployment },
)
: new RTClient({ key: apiKey }, { model });
const modalities: Modality[] =
modality === "audio" ? ["text", "audio"] : ["text"];
const modalities: Modality[]
= modality === 'audio' ? ['text', 'audio'] : ['text'];
const turnDetection: TurnDetection = useVAD
? { type: "server_vad" }
? { type: 'server_vad' }
: null;
await clientRef.current.configure({
instructions: "",
instructions: '',
voice,
input_audio_transcription: { model: "whisper-1" },
input_audio_transcription: { model: 'whisper-1' },
turn_detection: turnDetection,
tools: [],
temperature,
@ -108,8 +111,8 @@ export function RealtimeChat({
// console.error("Set message failed:", error);
// }
} catch (error) {
console.error("Connection failed:", error);
setStatus("Connection failed");
console.error('Connection failed:', error);
setStatus('Connection failed');
} finally {
setIsConnecting(false);
}
@ -125,35 +128,36 @@ export function RealtimeChat({
clientRef.current = null;
setIsConnected(false);
} catch (error) {
console.error("Disconnect failed:", error);
console.error('Disconnect failed:', error);
}
}
};
const startResponseListener = async () => {
if (!clientRef.current) return;
if (!clientRef.current)
{ return; }
try {
for await (const serverEvent of clientRef.current.events()) {
if (serverEvent.type === "response") {
if (serverEvent.type === 'response') {
await handleResponse(serverEvent);
} else if (serverEvent.type === "input_audio") {
} else if (serverEvent.type === 'input_audio') {
await handleInputAudio(serverEvent);
}
}
} catch (error) {
if (clientRef.current) {
console.error("Response iteration error:", error);
console.error('Response iteration error:', error);
}
}
};
const handleResponse = async (response: RTResponse) => {
for await (const item of response) {
if (item.type === "message" && item.role === "assistant") {
if (item.type === 'message' && item.role === 'assistant') {
const botMessage = createMessage({
role: item.role,
content: "",
content: '',
});
// add bot message first
chatStore.updateTargetSession(session, (session) => {
@ -161,11 +165,11 @@ export function RealtimeChat({
});
let hasAudio = false;
for await (const content of item) {
if (content.type === "text") {
if (content.type === 'text') {
for await (const text of content.textChunks()) {
botMessage.content += text;
}
} else if (content.type === "audio") {
} else if (content.type === 'audio') {
const textTask = async () => {
for await (const text of content.transcriptChunks()) {
botMessage.content += text;
@ -204,7 +208,7 @@ export function RealtimeChat({
await item.waitForCompletion();
if (item.transcription) {
const userMessage = createMessage({
role: "user",
role: 'user',
content: item.transcription,
});
chatStore.updateTargetSession(session, (session) => {
@ -240,7 +244,7 @@ export function RealtimeChat({
});
setIsRecording(true);
} catch (error) {
console.error("Failed to start recording:", error);
console.error('Failed to start recording:', error);
}
} else if (audioHandlerRef.current) {
try {
@ -252,14 +256,15 @@ export function RealtimeChat({
}
setIsRecording(false);
} catch (error) {
console.error("Failed to stop recording:", error);
console.error('Failed to stop recording:', error);
}
}
};
useEffect(() => {
// 防止重复初始化
if (initRef.current) return;
if (initRef.current)
{ return; }
initRef.current = true;
const initAudioHandler = async () => {
@ -325,16 +330,16 @@ export function RealtimeChat({
};
return (
<div className={styles["realtime-chat"]}>
<div className={styles['realtime-chat']}>
<div
className={clsx(styles["circle-mic"], {
[styles["pulse"]]: isRecording,
className={clsx(styles['circle-mic'], {
[styles.pulse]: isRecording,
})}
>
<VoicePrint frequencies={frequencies} isActive={isRecording} />
</div>
<div className={styles["bottom-icons"]}>
<div className={styles['bottom-icons']}>
<div>
<IconButton
icon={isRecording ? <VoiceIcon /> : <VoiceOffIcon />}
@ -344,7 +349,7 @@ export function RealtimeChat({
bordered
/>
</div>
<div className={styles["icon-center"]}>{status}</div>
<div className={styles['icon-center']}>{status}</div>
<div>
<IconButton
icon={<PowerIcon />}

View File

@ -1,24 +1,24 @@
import { RealtimeConfig } from "@/app/store";
import type { RealtimeConfig } from '@/app/store';
import Locale from "@/app/locales";
import { ListItem, Select, PasswordInput } from "@/app/components/ui-lib";
import type { Voice } from 'rt-client';
import { InputRange } from '@/app/components/input-range';
import { InputRange } from "@/app/components/input-range";
import { Voice } from "rt-client";
import { ServiceProvider } from "@/app/constant";
import { ListItem, PasswordInput, Select } from '@/app/components/ui-lib';
import { ServiceProvider } from '@/app/constant';
import Locale from '@/app/locales';
const providers = [ServiceProvider.OpenAI, ServiceProvider.Azure];
const models = ["gpt-4o-realtime-preview-2024-10-01"];
const models = ['gpt-4o-realtime-preview-2024-10-01'];
const voice = ["alloy", "shimmer", "echo"];
const voice = ['alloy', 'shimmer', 'echo'];
export function RealtimeConfigList(props: {
realtimeConfig: RealtimeConfig;
updateConfig: (updater: (config: RealtimeConfig) => void) => void;
}) {
const azureConfigComponent = props.realtimeConfig.provider ===
ServiceProvider.Azure && (
const azureConfigComponent = props.realtimeConfig.provider
=== ServiceProvider.Azure && (
<>
<ListItem
title={Locale.Settings.Realtime.Azure.Endpoint.Title}
@ -30,7 +30,7 @@ export function RealtimeConfigList(props: {
placeholder={Locale.Settings.Realtime.Azure.Endpoint.Title}
onChange={(e) => {
props.updateConfig(
(config) => (config.azure.endpoint = e.currentTarget.value),
config => (config.azure.endpoint = e.currentTarget.value),
);
}}
/>
@ -45,7 +45,7 @@ export function RealtimeConfigList(props: {
placeholder={Locale.Settings.Realtime.Azure.Deployment.Title}
onChange={(e) => {
props.updateConfig(
(config) => (config.azure.deployment = e.currentTarget.value),
config => (config.azure.deployment = e.currentTarget.value),
);
}}
/>
@ -62,12 +62,12 @@ export function RealtimeConfigList(props: {
<input
type="checkbox"
checked={props.realtimeConfig.enable}
onChange={(e) =>
onChange={e =>
props.updateConfig(
(config) => (config.enable = e.currentTarget.checked),
)
}
></input>
config => (config.enable = e.currentTarget.checked),
)}
>
</input>
</ListItem>
{props.realtimeConfig.enable && (
@ -81,7 +81,7 @@ export function RealtimeConfigList(props: {
value={props.realtimeConfig.provider}
onChange={(e) => {
props.updateConfig(
(config) =>
config =>
(config.provider = e.target.value as ServiceProvider),
);
}}
@ -101,7 +101,7 @@ export function RealtimeConfigList(props: {
aria-label={Locale.Settings.Realtime.Model.Title}
value={props.realtimeConfig.model}
onChange={(e) => {
props.updateConfig((config) => (config.model = e.target.value));
props.updateConfig(config => (config.model = e.target.value));
}}
>
{models.map((v, i) => (
@ -123,7 +123,7 @@ export function RealtimeConfigList(props: {
placeholder={Locale.Settings.Realtime.ApiKey.Placeholder}
onChange={(e) => {
props.updateConfig(
(config) => (config.apiKey = e.currentTarget.value),
config => (config.apiKey = e.currentTarget.value),
);
}}
/>
@ -137,7 +137,7 @@ export function RealtimeConfigList(props: {
value={props.realtimeConfig.voice}
onChange={(e) => {
props.updateConfig(
(config) => (config.voice = e.currentTarget.value as Voice),
config => (config.voice = e.currentTarget.value as Voice),
);
}}
>
@ -160,11 +160,12 @@ export function RealtimeConfigList(props: {
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
config =>
(config.temperature = e.currentTarget.valueAsNumber),
);
}}
></InputRange>
>
</InputRange>
</ListItem>
</>
)}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,86 +1,90 @@
import chatStyles from "@/app/components/chat.module.scss";
import styles from "@/app/components/sd/sd.module.scss";
import homeStyles from "@/app/components/home.module.scss";
import type { Property } from 'csstype';
import { IconButton } from '@/app/components/button';
import { ChatAction } from '@/app/components/chat';
import { IconButton } from "@/app/components/button";
import ReturnIcon from "@/app/icons/return.svg";
import Locale from "@/app/locales";
import { Path } from "@/app/constant";
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
copyToClipboard,
getMessageTextContent,
useMobileScreen,
} from "@/app/utils";
import { useNavigate, useLocation } from "react-router-dom";
import { useAppConfig } from "@/app/store";
import MinIcon from "@/app/icons/min.svg";
import MaxIcon from "@/app/icons/max.svg";
import { getClientConfig } from "@/app/config/client";
import { ChatAction } from "@/app/components/chat";
import DeleteIcon from "@/app/icons/clear.svg";
import CopyIcon from "@/app/icons/copy.svg";
import PromptIcon from "@/app/icons/prompt.svg";
import ResetIcon from "@/app/icons/reload.svg";
import { useSdStore } from "@/app/store/sd";
import LoadingIcon from "@/app/icons/three-dots.svg";
import ErrorIcon from "@/app/icons/delete.svg";
import SDIcon from "@/app/icons/sd.svg";
import { Property } from "csstype";
import chatStyles from '@/app/components/chat.module.scss';
import { WindowContent } from '@/app/components/home';
import homeStyles from '@/app/components/home.module.scss';
import styles from '@/app/components/sd/sd.module.scss';
import {
showConfirm,
showImageModal,
showModal,
} from "@/app/components/ui-lib";
import { removeImage } from "@/app/utils/chat";
import { SideBar } from "./sd-sidebar";
import { WindowContent } from "@/app/components/home";
import { params } from "./sd-panel";
import clsx from "clsx";
} from '@/app/components/ui-lib';
import { getClientConfig } from '@/app/config/client';
import { Path } from '@/app/constant';
import DeleteIcon from '@/app/icons/clear.svg';
import CopyIcon from '@/app/icons/copy.svg';
import ErrorIcon from '@/app/icons/delete.svg';
import MaxIcon from '@/app/icons/max.svg';
import MinIcon from '@/app/icons/min.svg';
import PromptIcon from '@/app/icons/prompt.svg';
import ResetIcon from '@/app/icons/reload.svg';
import ReturnIcon from '@/app/icons/return.svg';
import SDIcon from '@/app/icons/sd.svg';
import LoadingIcon from '@/app/icons/three-dots.svg';
import Locale from '@/app/locales';
import { useAppConfig } from '@/app/store';
import { useSdStore } from '@/app/store/sd';
import {
copyToClipboard,
getMessageTextContent,
useMobileScreen,
} from '@/app/utils';
import { removeImage } from '@/app/utils/chat';
import clsx from 'clsx';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { params } from './sd-panel';
import { SideBar } from './sd-sidebar';
function getSdTaskStatus(item: any) {
let s: string;
let color: Property.Color | undefined = undefined;
let color: Property.Color | undefined;
switch (item.status) {
case "success":
case 'success':
s = Locale.Sd.Status.Success;
color = "green";
color = 'green';
break;
case "error":
case 'error':
s = Locale.Sd.Status.Error;
color = "red";
color = 'red';
break;
case "wait":
case 'wait':
s = Locale.Sd.Status.Wait;
color = "yellow";
color = 'yellow';
break;
case "running":
case 'running':
s = Locale.Sd.Status.Running;
color = "blue";
color = 'blue';
break;
default:
s = item.status.toUpperCase();
}
return (
<p className={styles["line-1"]} title={item.error} style={{ color: color }}>
<p className={styles['line-1']} title={item.error} style={{ color }}>
<span>
{Locale.Sd.Status.Name}: {s}
{Locale.Sd.Status.Name}
:
{s}
</span>
{item.status === "error" && (
{item.status === 'error' && (
<span
className="clickable"
onClick={() => {
showModal({
title: Locale.Sd.Detail,
children: (
<div style={{ color: color, userSelect: "text" }}>
<div style={{ color, userSelect: 'text' }}>
{item.error}
</div>
),
});
}}
>
- {item.error}
-
{' '}
{item.error}
</span>
)}
</p>
@ -105,13 +109,13 @@ export function Sd() {
return (
<>
<SideBar className={clsx({ [homeStyles["sidebar-show"]]: isSd })} />
<SideBar className={clsx({ [homeStyles['sidebar-show']]: isSd })} />
<WindowContent>
<div className={chatStyles.chat} key={"1"}>
<div className={chatStyles.chat} key="1">
<div className="window-header" data-tauri-drag-region>
{isMobileScreen && (
<div className="window-actions">
<div className={"window-action-button"}>
<div className="window-action-button">
<IconButton
icon={<ReturnIcon />}
bordered
@ -123,11 +127,11 @@ export function Sd() {
)}
<div
className={clsx(
"window-header-title",
chatStyles["chat-body-title"],
'window-header-title',
chatStyles['chat-body-title'],
)}
>
<div className={`window-header-main-title`}>Stability AI</div>
<div className="window-header-main-title">Stability AI</div>
<div className="window-header-sub-title">
{Locale.Sd.SubTitle(sdImages.length || 0)}
</div>
@ -142,7 +146,7 @@ export function Sd() {
bordered
onClick={() => {
config.update(
(config) => (config.tightBorder = !config.tightBorder),
config => (config.tightBorder = !config.tightBorder),
);
}}
/>
@ -151,49 +155,54 @@ export function Sd() {
{isMobileScreen && <SDIcon width={50} height={50} />}
</div>
</div>
<div className={chatStyles["chat-body"]} ref={scrollRef}>
<div className={styles["sd-img-list"]}>
<div className={chatStyles['chat-body']} ref={scrollRef}>
<div className={styles['sd-img-list']}>
{sdImages.length > 0 ? (
sdImages.map((item: any) => {
return (
<div
key={item.id}
style={{ display: "flex" }}
className={styles["sd-img-item"]}
style={{ display: 'flex' }}
className={styles['sd-img-item']}
>
{item.status === "success" ? (
<img
className={styles["img"]}
src={item.img_data}
alt={item.id}
onClick={(e) =>
showImageModal(
item.img_data,
true,
isMobileScreen
? { width: "100%", height: "fit-content" }
: { maxWidth: "100%", maxHeight: "100%" },
isMobileScreen
? { width: "100%", height: "fit-content" }
: { width: "100%", height: "100%" },
{item.status === 'success'
? (
<img
className={styles.img}
src={item.img_data}
alt={item.id}
onClick={e =>
showImageModal(
item.img_data,
true,
isMobileScreen
? { width: '100%', height: 'fit-content' }
: { maxWidth: '100%', maxHeight: '100%' },
isMobileScreen
? { width: '100%', height: 'fit-content' }
: { width: '100%', height: '100%' },
)}
/>
)
: item.status === 'error'
? (
<div className={styles['pre-img']}>
<ErrorIcon />
</div>
)
}
/>
) : item.status === "error" ? (
<div className={styles["pre-img"]}>
<ErrorIcon />
</div>
) : (
<div className={styles["pre-img"]}>
<LoadingIcon />
</div>
)}
: (
<div className={styles['pre-img']}>
<LoadingIcon />
</div>
)}
<div
style={{ marginLeft: "10px" }}
className={styles["sd-img-item-info"]}
style={{ marginLeft: '10px' }}
className={styles['sd-img-item-info']}
>
<p className={styles["line-1"]}>
{Locale.SdPanel.Prompt}:{" "}
<p className={styles['line-1']}>
{Locale.SdPanel.Prompt}
:
{' '}
<span
className="clickable"
title={item.params.prompt}
@ -201,7 +210,7 @@ export function Sd() {
showModal({
title: Locale.Sd.Detail,
children: (
<div style={{ userSelect: "text" }}>
<div style={{ userSelect: 'text' }}>
{item.params.prompt}
</div>
),
@ -212,12 +221,14 @@ export function Sd() {
</span>
</p>
<p>
{Locale.SdPanel.AIModel}: {item.model_name}
{Locale.SdPanel.AIModel}
:
{item.model_name}
</p>
{getSdTaskStatus(item)}
<p>{item.created_at}</p>
<div className={chatStyles["chat-message-actions"]}>
<div className={chatStyles["chat-input-actions"]}>
<div className={chatStyles['chat-message-actions']}>
<div className={chatStyles['chat-input-actions']}>
<ChatAction
text={Locale.Sd.Actions.Params}
icon={<PromptIcon />}
@ -225,39 +236,41 @@ export function Sd() {
showModal({
title: Locale.Sd.GenerateParams,
children: (
<div style={{ userSelect: "text" }}>
<div style={{ userSelect: 'text' }}>
{Object.keys(item.params).map((key) => {
let label = key;
let value = item.params[key];
switch (label) {
case "prompt":
case 'prompt':
label = Locale.SdPanel.Prompt;
break;
case "negative_prompt":
label =
Locale.SdPanel.NegativePrompt;
case 'negative_prompt':
label
= Locale.SdPanel.NegativePrompt;
break;
case "aspect_ratio":
case 'aspect_ratio':
label = Locale.SdPanel.AspectRatio;
break;
case "seed":
label = "Seed";
case 'seed':
label = 'Seed';
value = value || 0;
break;
case "output_format":
case 'output_format':
label = Locale.SdPanel.OutFormat;
value = value?.toUpperCase();
break;
case "style":
case 'style':
label = Locale.SdPanel.ImageStyle;
value = params
.find(
(item) =>
item.value === "style",
item =>
item.value === 'style',
)
?.options?.find(
(item) => item.value === value,
)?.name;
?.options
?.find(
item => item.value === value,
)
?.name;
break;
default:
break;
@ -266,9 +279,13 @@ export function Sd() {
return (
<div
key={key}
style={{ margin: "10px" }}
style={{ margin: '10px' }}
>
<strong>{label}: </strong>
<strong>
{label}
:
{' '}
</strong>
{value}
</div>
);
@ -284,11 +301,10 @@ export function Sd() {
onClick={() =>
copyToClipboard(
getMessageTextContent({
role: "user",
role: 'user',
content: item.params.prompt,
}),
)
}
)}
/>
<ChatAction
text={Locale.Sd.Actions.Retry}
@ -297,10 +313,10 @@ export function Sd() {
const reqData = {
model: item.model,
model_name: item.model_name,
status: "wait",
status: 'wait',
params: { ...item.params },
created_at: new Date().toLocaleString(),
img_data: "",
img_data: '',
};
sdStore.sendTask(reqData);
}}

View File

@ -1,20 +1,20 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
import { useNavigate } from "react-router-dom";
import { IconButton } from "./button";
import CloseIcon from "../icons/close.svg";
import EyeIcon from "../icons/eye.svg";
import Locale from "../locales";
import { Path } from "../constant";
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Path } from '../constant';
import CloseIcon from '../icons/close.svg';
import EyeIcon from '../icons/eye.svg';
import Locale from '../locales';
import { useChatStore } from '../store';
import { IconButton } from './button';
import { ErrorBoundary } from './error';
import { useChatStore } from "../store";
import styles from './mask.module.scss';
type Item = {
interface Item {
id: number;
name: string;
content: string;
};
}
export function SearchChatPage() {
const navigate = useNavigate();
@ -25,7 +25,7 @@ export function SearchChatPage() {
const [searchResults, setSearchResults] = useState<Item[]>([]);
const previousValueRef = useRef<string>("");
const previousValueRef = useRef<string>('');
const searchInputRef = useRef<HTMLInputElement>(null);
const doSearch = useCallback((text: string) => {
const lowerCaseText = text.toLowerCase();
@ -36,7 +36,8 @@ export function SearchChatPage() {
session.messages.forEach((message) => {
const content = message.content as string;
if (!content.toLowerCase || content === "") return;
if (!content.toLowerCase || content === '')
{ return; }
const lowerCaseContent = content.toLowerCase();
// full text search
@ -56,7 +57,7 @@ export function SearchChatPage() {
results.push({
id: index,
name: session.topic,
content: fullTextContents.join("... "), // concat content with...
content: fullTextContents.join('... '), // concat content with...
});
}
});
@ -87,7 +88,7 @@ export function SearchChatPage() {
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
<div className={styles['mask-page']}>
{/* header */}
<div className="window-header">
<div className="window-header-title">
@ -110,17 +111,17 @@ export function SearchChatPage() {
</div>
</div>
<div className={styles["mask-page-body"]}>
<div className={styles["mask-filter"]}>
{/**搜索输入框 */}
<div className={styles['mask-page-body']}>
<div className={styles['mask-filter']}>
{/** 搜索输入框 */}
<input
type="text"
className={styles["search-bar"]}
className={styles['search-bar']}
placeholder={Locale.SearchChat.Page.Search}
autoFocus
ref={searchInputRef}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.key === 'Enter') {
e.preventDefault();
const searchText = e.currentTarget.value;
if (searchText.length > 0) {
@ -133,25 +134,25 @@ export function SearchChatPage() {
</div>
<div>
{searchResults.map((item) => (
{searchResults.map(item => (
<div
className={styles["mask-item"]}
className={styles['mask-item']}
key={item.id}
onClick={() => {
navigate(Path.Chat);
selectSession(item.id);
}}
style={{ cursor: "pointer" }}
style={{ cursor: 'pointer' }}
>
{/** 搜索匹配的文本 */}
<div className={styles["mask-header"]}>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{item.name}</div>
<div className={styles['mask-header']}>
<div className={styles['mask-title']}>
<div className={styles['mask-name']}>{item.name}</div>
{item.content.slice(0, 70)}
</div>
</div>
{/** 操作按钮 */}
<div className={styles["mask-actions"]}>
<div className={styles['mask-actions']}>
<IconButton
icon={<EyeIcon />}
text={Locale.SearchChat.Item.View}

View File

@ -75,6 +75,6 @@
.subtitle-button {
button {
overflow:visible ;
overflow: visible;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,9 @@
import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
import clsx from 'clsx';
import styles from "./home.module.scss";
import { IconButton } from "./button";
import SettingsIcon from "../icons/settings.svg";
import GithubIcon from "../icons/github.svg";
import ChatGptIcon from "../icons/chatgpt.svg";
import AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg";
import MaskIcon from "../icons/mask.svg";
import DragIcon from "../icons/drag.svg";
import DiscoveryIcon from "../icons/discovery.svg";
import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store";
import dynamic from 'next/dynamic';
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import {
DEFAULT_SIDEBAR_WIDTH,
MAX_SIDEBAR_WIDTH,
@ -24,15 +12,27 @@ import {
Path,
PLUGINS,
REPO_URL,
} from "../constant";
} from '../constant';
import AddIcon from '../icons/add.svg';
import ChatGptIcon from '../icons/chatgpt.svg';
import DeleteIcon from '../icons/delete.svg';
import DiscoveryIcon from '../icons/discovery.svg';
import DragIcon from '../icons/drag.svg';
import GithubIcon from '../icons/github.svg';
import { Link, useNavigate } from "react-router-dom";
import { isIOS, useMobileScreen } from "../utils";
import dynamic from "next/dynamic";
import { showConfirm, Selector } from "./ui-lib";
import clsx from "clsx";
import MaskIcon from '../icons/mask.svg';
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
import SettingsIcon from '../icons/settings.svg';
import Locale from '../locales';
import { useAppConfig, useChatStore } from '../store';
import { isIOS, useMobileScreen } from '../utils';
import { IconButton } from './button';
import styles from './home.module.scss';
import { Selector, showConfirm } from './ui-lib';
const ChatList = dynamic(async () => (await import('./chat-list')).ChatList, {
loading: () => null,
});
@ -42,16 +42,16 @@ export function useHotKey() {
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.altKey || e.ctrlKey) {
if (e.key === "ArrowUp") {
if (e.key === 'ArrowUp') {
chatStore.nextSession(-1);
} else if (e.key === "ArrowDown") {
} else if (e.key === 'ArrowDown') {
chatStore.nextSession(1);
}
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
});
}
@ -97,8 +97,8 @@ export function useDragSideBar() {
const handleDragEnd = () => {
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
window.removeEventListener("pointermove", handleDragMove);
window.removeEventListener("pointerup", handleDragEnd);
window.removeEventListener('pointermove', handleDragMove);
window.removeEventListener('pointerup', handleDragEnd);
// if user click the drag icon, should toggle the sidebar
const shouldFireClick = Date.now() - dragStartTime < 300;
@ -107,20 +107,20 @@ export function useDragSideBar() {
}
};
window.addEventListener("pointermove", handleDragMove);
window.addEventListener("pointerup", handleDragEnd);
window.addEventListener('pointermove', handleDragMove);
window.addEventListener('pointerup', handleDragEnd);
};
const isMobileScreen = useMobileScreen();
const shouldNarrow =
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
const shouldNarrow
= !isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
useEffect(() => {
const barWidth = shouldNarrow
? NARROW_SIDEBAR_WIDTH
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
const sideBarWidth = isMobileScreen ? '100vw' : `${barWidth}px`;
document.documentElement.style.setProperty('--sidebar-width', sideBarWidth);
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
return {
@ -143,17 +143,17 @@ export function SideBarContainer(props: {
return (
<div
className={clsx(styles.sidebar, className, {
[styles["narrow-sidebar"]]: shouldNarrow,
[styles['narrow-sidebar']]: shouldNarrow,
})}
style={{
// #3016 disable transition on ios mobile screen
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
transition: isMobileScreen && isIOSMobile ? 'none' : undefined,
}}
>
{children}
<div
className={styles["sidebar-drag"]}
onPointerDown={(e) => onDragStart(e as any)}
className={styles['sidebar-drag']}
onPointerDown={e => onDragStart(e as any)}
>
<DragIcon />
</div>
@ -172,18 +172,18 @@ export function SideBarHeader(props: {
return (
<Fragment>
<div
className={clsx(styles["sidebar-header"], {
[styles["sidebar-header-narrow"]]: shouldNarrow,
className={clsx(styles['sidebar-header'], {
[styles['sidebar-header-narrow']]: shouldNarrow,
})}
data-tauri-drag-region
>
<div className={styles["sidebar-title-container"]}>
<div className={styles["sidebar-title"]} data-tauri-drag-region>
<div className={styles['sidebar-title-container']}>
<div className={styles['sidebar-title']} data-tauri-drag-region>
{title}
</div>
<div className={styles["sidebar-sub-title"]}>{subTitle}</div>
<div className={styles['sidebar-sub-title']}>{subTitle}</div>
</div>
<div className={clsx(styles["sidebar-logo"], "no-dark")}>{logo}</div>
<div className={clsx(styles['sidebar-logo'], 'no-dark')}>{logo}</div>
</div>
{children}
</Fragment>
@ -196,7 +196,7 @@ export function SideBarBody(props: {
}) {
const { onClick, children } = props;
return (
<div className={styles["sidebar-body"]} onClick={onClick}>
<div className={styles['sidebar-body']} onClick={onClick}>
{children}
</div>
);
@ -209,9 +209,9 @@ export function SideBarTail(props: {
const { primaryAction, secondaryAction } = props;
return (
<div className={styles["sidebar-tail"]}>
<div className={styles["sidebar-actions"]}>{primaryAction}</div>
<div className={styles["sidebar-actions"]}>{secondaryAction}</div>
<div className={styles['sidebar-tail']}>
<div className={styles['sidebar-actions']}>{primaryAction}</div>
<div className={styles['sidebar-actions']}>{secondaryAction}</div>
</div>
);
}
@ -236,11 +236,11 @@ export function SideBar(props: { className?: string }) {
logo={<ChatGptIcon />}
shouldNarrow={shouldNarrow}
>
<div className={styles["sidebar-header-bar"]}>
<div className={styles['sidebar-header-bar']}>
<IconButton
icon={<MaskIcon />}
text={shouldNarrow ? undefined : Locale.Mask.Name}
className={styles["sidebar-bar-button"]}
className={styles['sidebar-bar-button']}
onClick={() => {
if (config.dontShowMaskSplashScreen !== true) {
navigate(Path.NewChat, { state: { fromHome: true } });
@ -253,7 +253,7 @@ export function SideBar(props: { className?: string }) {
<IconButton
icon={<DiscoveryIcon />}
text={shouldNarrow ? undefined : Locale.Discovery.Name}
className={styles["sidebar-bar-button"]}
className={styles['sidebar-bar-button']}
onClick={() => setShowPluginSelector(true)}
shadow
/>
@ -285,9 +285,9 @@ export function SideBar(props: { className?: string }) {
<ChatList narrow={shouldNarrow} />
</SideBarBody>
<SideBarTail
primaryAction={
primaryAction={(
<>
<div className={clsx(styles["sidebar-action"], styles.mobile)}>
<div className={clsx(styles['sidebar-action'], styles.mobile)}>
<IconButton
icon={<DeleteIcon />}
onClick={async () => {
@ -297,7 +297,7 @@ export function SideBar(props: { className?: string }) {
}}
/>
</div>
<div className={styles["sidebar-action"]}>
<div className={styles['sidebar-action']}>
<Link to={Path.Settings}>
<IconButton
aria={Locale.Settings.Title}
@ -306,7 +306,7 @@ export function SideBar(props: { className?: string }) {
/>
</Link>
</div>
<div className={styles["sidebar-action"]}>
<div className={styles['sidebar-action']}>
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
<IconButton
aria={Locale.Export.MessageFromChatGPT}
@ -316,8 +316,8 @@ export function SideBar(props: { className?: string }) {
</a>
</div>
</>
}
secondaryAction={
)}
secondaryAction={(
<IconButton
icon={<AddIcon />}
text={shouldNarrow ? undefined : Locale.Home.NewChat}
@ -331,7 +331,7 @@ export function SideBar(props: { className?: string }) {
}}
shadow
/>
}
)}
/>
</SideBarContainer>
);

View File

@ -1,14 +1,15 @@
import { TTSConfig, TTSConfigValidator } from "../store";
import Locale from "../locales";
import { ListItem, Select } from "./ui-lib";
import type { TTSConfig } from '../store';
import {
DEFAULT_TTS_ENGINE,
DEFAULT_TTS_ENGINES,
DEFAULT_TTS_MODELS,
DEFAULT_TTS_VOICES,
} from "../constant";
import { InputRange } from "./input-range";
} from '../constant';
import Locale from '../locales';
import { TTSConfigValidator } from '../store';
import { InputRange } from './input-range';
import { ListItem, Select } from './ui-lib';
export function TTSConfigList(props: {
ttsConfig: TTSConfig;
@ -23,12 +24,12 @@ export function TTSConfigList(props: {
<input
type="checkbox"
checked={props.ttsConfig.enable}
onChange={(e) =>
onChange={e =>
props.updateConfig(
(config) => (config.enable = e.currentTarget.checked),
)
}
></input>
config => (config.enable = e.currentTarget.checked),
)}
>
</input>
</ListItem>
{/* <ListItem
title={Locale.Settings.TTS.Autoplay.Title}
@ -49,7 +50,7 @@ export function TTSConfigList(props: {
value={props.ttsConfig.engine}
onChange={(e) => {
props.updateConfig(
(config) =>
config =>
(config.engine = TTSConfigValidator.engine(
e.currentTarget.value,
)),
@ -70,7 +71,7 @@ export function TTSConfigList(props: {
value={props.ttsConfig.model}
onChange={(e) => {
props.updateConfig(
(config) =>
config =>
(config.model = TTSConfigValidator.model(
e.currentTarget.value,
)),
@ -92,7 +93,7 @@ export function TTSConfigList(props: {
value={props.ttsConfig.voice}
onChange={(e) => {
props.updateConfig(
(config) =>
config =>
(config.voice = TTSConfigValidator.voice(
e.currentTarget.value,
)),
@ -118,13 +119,14 @@ export function TTSConfigList(props: {
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
config =>
(config.speed = TTSConfigValidator.speed(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
>
</InputRange>
</ListItem>
</>
)}

View File

@ -336,4 +336,3 @@
}
}
}

View File

@ -1,29 +1,30 @@
/* eslint-disable @next/next/no-img-element */
import styles from "./ui-lib.module.scss";
import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg";
import DownIcon from "../icons/down.svg";
import ConfirmIcon from "../icons/confirm.svg";
import CancelIcon from "../icons/cancel.svg";
import MaxIcon from "../icons/max.svg";
import MinIcon from "../icons/min.svg";
import Locale from "../locales";
import { createRoot } from "react-dom/client";
import React, {
import type {
CSSProperties,
HTMLProps,
MouseEvent,
useEffect,
useState,
} from 'react';
import clsx from 'clsx';
import React, {
useCallback,
useEffect,
useRef,
} from "react";
import { IconButton } from "./button";
import clsx from "clsx";
useState,
} from 'react';
import { createRoot } from 'react-dom/client';
import CancelIcon from '../icons/cancel.svg';
import CloseIcon from '../icons/close.svg';
import ConfirmIcon from '../icons/confirm.svg';
import DownIcon from '../icons/down.svg';
import EyeIcon from '../icons/eye.svg';
import EyeOffIcon from '../icons/eye-off.svg';
import MaxIcon from '../icons/max.svg';
import MinIcon from '../icons/min.svg';
import LoadingIcon from '../icons/three-dots.svg';
import Locale from '../locales';
import { IconButton } from './button';
import styles from './ui-lib.module.scss';
export function Popover(props: {
children: JSX.Element;
@ -35,10 +36,10 @@ export function Popover(props: {
<div className={styles.popover}>
{props.children}
{props.open && (
<div className={styles["popover-mask"]} onClick={props.onClose}></div>
<div className={styles['popover-mask']} onClick={props.onClose}></div>
)}
{props.open && (
<div className={styles["popover-content"]}>{props.content}</div>
<div className={styles['popover-content']}>{props.content}</div>
)}
</div>
);
@ -62,20 +63,20 @@ export function ListItem(props: {
return (
<div
className={clsx(
styles["list-item"],
styles['list-item'],
{
[styles["vertical"]]: props.vertical,
[styles.vertical]: props.vertical,
},
props.className,
)}
onClick={props.onClick}
>
<div className={styles["list-header"]}>
{props.icon && <div className={styles["list-icon"]}>{props.icon}</div>}
<div className={styles["list-item-title"]}>
<div className={styles['list-header']}>
{props.icon && <div className={styles['list-icon']}>{props.icon}</div>}
<div className={styles['list-item-title']}>
<div>{props.title}</div>
{props.subTitle && (
<div className={styles["list-item-sub-title"]}>
<div className={styles['list-item-sub-title']}>
{props.subTitle}
</div>
)}
@ -98,11 +99,11 @@ export function Loading() {
return (
<div
style={{
height: "100vh",
width: "100vw",
display: "flex",
alignItems: "center",
justifyContent: "center",
height: '100vh',
width: '100vw',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<LoadingIcon />
@ -121,15 +122,15 @@ interface ModalProps {
export function Modal(props: ModalProps) {
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
if (e.key === 'Escape') {
props.onClose?.();
}
};
window.addEventListener("keydown", onKeyDown);
window.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener('keydown', onKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -138,22 +139,22 @@ export function Modal(props: ModalProps) {
return (
<div
className={clsx(styles["modal-container"], {
[styles["modal-container-max"]]: isMax,
className={clsx(styles['modal-container'], {
[styles['modal-container-max']]: isMax,
})}
>
<div className={styles["modal-header"]}>
<div className={styles["modal-title"]}>{props.title}</div>
<div className={styles['modal-header']}>
<div className={styles['modal-title']}>{props.title}</div>
<div className={styles["modal-header-actions"]}>
<div className={styles['modal-header-actions']}>
<div
className={styles["modal-header-action"]}
className={styles['modal-header-action']}
onClick={() => setMax(!isMax)}
>
{isMax ? <MinIcon /> : <MaxIcon />}
</div>
<div
className={styles["modal-header-action"]}
className={styles['modal-header-action']}
onClick={props.onClose}
>
<CloseIcon />
@ -161,13 +162,13 @@ export function Modal(props: ModalProps) {
</div>
</div>
<div className={styles["modal-content"]}>{props.children}</div>
<div className={styles['modal-content']}>{props.children}</div>
<div className={styles["modal-footer"]}>
<div className={styles['modal-footer']}>
{props.footer}
<div className={styles["modal-actions"]}>
<div className={styles['modal-actions']}>
{props.actions?.map((action, i) => (
<div key={i} className={styles["modal-action"]}>
<div key={i} className={styles['modal-action']}>
{action}
</div>
))}
@ -178,8 +179,8 @@ export function Modal(props: ModalProps) {
}
export function showModal(props: ModalProps) {
const div = document.createElement("div");
div.className = "modal-mask";
const div = document.createElement('div');
div.className = 'modal-mask';
document.body.appendChild(div);
const root = createRoot(div);
@ -198,19 +199,19 @@ export function showModal(props: ModalProps) {
root.render(<Modal {...props} onClose={closeModal}></Modal>);
}
export type ToastProps = {
export interface ToastProps {
content: string;
action?: {
text: string;
onClick: () => void;
};
onClose?: () => void;
};
}
export function Toast(props: ToastProps) {
return (
<div className={styles["toast-container"]}>
<div className={styles["toast-content"]}>
<div className={styles['toast-container']}>
<div className={styles['toast-content']}>
<span>{props.content}</span>
{props.action && (
<button
@ -218,7 +219,7 @@ export function Toast(props: ToastProps) {
props.action?.onClick?.();
props.onClose?.();
}}
className={styles["toast-action"]}
className={styles['toast-action']}
>
{props.action.text}
</button>
@ -230,10 +231,10 @@ export function Toast(props: ToastProps) {
export function showToast(
content: string,
action?: ToastProps["action"],
action?: ToastProps['action'],
delay = 3000,
) {
const div = document.createElement("div");
const div = document.createElement('div');
div.className = styles.show;
document.body.appendChild(div);
@ -263,8 +264,9 @@ export function Input(props: InputProps) {
return (
<textarea
{...props}
className={clsx(styles["input"], props.className)}
></textarea>
className={clsx(styles.input, props.className)}
>
</textarea>
);
}
@ -277,17 +279,17 @@ export function PasswordInput(
}
return (
<div className={"password-input-container"}>
<div className="password-input-container">
<IconButton
aria={props.aria}
icon={visible ? <EyeIcon /> : <EyeOffIcon />}
onClick={changeVisibility}
className={"password-eye"}
className="password-eye"
/>
<input
{...props}
type={visible ? "text" : "password"}
className={"password-input"}
type={visible ? 'text' : 'password'}
className="password-input"
/>
</div>
);
@ -296,7 +298,7 @@ export function PasswordInput(
export function Select(
props: React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement> & {
align?: "left" | "center";
align?: 'left' | 'center';
},
HTMLSelectElement
>,
@ -305,24 +307,24 @@ export function Select(
return (
<div
className={clsx(
styles["select-with-icon"],
styles['select-with-icon'],
{
[styles["left-align-option"]]: align === "left",
[styles['left-align-option']]: align === 'left',
},
className,
)}
>
<select className={styles["select-with-icon-select"]} {...otherProps}>
<select className={styles['select-with-icon-select']} {...otherProps}>
{children}
</select>
<DownIcon className={styles["select-with-icon-icon"]} />
<DownIcon className={styles['select-with-icon-icon']} />
</div>
);
}
export function showConfirm(content: any) {
const div = document.createElement("div");
div.className = "modal-mask";
const div = document.createElement('div');
div.className = 'modal-mask';
document.body.appendChild(div);
const root = createRoot(div);
@ -347,7 +349,8 @@ export function showConfirm(content: any) {
tabIndex={0}
bordered
shadow
></IconButton>,
>
</IconButton>,
<IconButton
key="confirm"
text={Locale.UI.Confirm}
@ -361,7 +364,8 @@ export function showConfirm(content: any) {
autoFocus
bordered
shadow
></IconButton>,
>
</IconButton>,
]}
onClose={closeModal}
>
@ -384,18 +388,19 @@ function PromptInput(props: {
return (
<textarea
className={styles["modal-input"]}
className={styles['modal-input']}
autoFocus
value={input}
onInput={(e) => onInput(e.currentTarget.value)}
onInput={e => onInput(e.currentTarget.value)}
rows={props.rows ?? 3}
></textarea>
>
</textarea>
);
}
export function showPrompt(content: any, value = "", rows = 3) {
const div = document.createElement("div");
div.className = "modal-mask";
export function showPrompt(content: any, value = '', rows = 3) {
const div = document.createElement('div');
div.className = 'modal-mask';
document.body.appendChild(div);
const root = createRoot(div);
@ -421,7 +426,8 @@ export function showPrompt(content: any, value = "", rows = 3) {
bordered
shadow
tabIndex={0}
></IconButton>,
>
</IconButton>,
<IconButton
key="confirm"
text={Locale.UI.Confirm}
@ -434,15 +440,17 @@ export function showPrompt(content: any, value = "", rows = 3) {
bordered
shadow
tabIndex={0}
></IconButton>,
>
</IconButton>,
]}
onClose={closeModal}
>
<PromptInput
onChange={(val) => (userInput = val)}
onChange={val => (userInput = val)}
value={value}
rows={rows}
></PromptInput>
>
</PromptInput>
</Modal>,
);
});
@ -456,18 +464,19 @@ export function showImageModal(
) {
showModal({
title: Locale.Export.Image.Modal,
defaultMax: defaultMax,
defaultMax,
children: (
<div style={{ display: "flex", justifyContent: "center", ...boxStyle }}>
<div style={{ display: 'flex', justifyContent: 'center', ...boxStyle }}>
<img
src={img}
alt="preview"
style={
style ?? {
maxWidth: "100%",
maxWidth: '100%',
}
}
></img>
>
</img>
</div>
),
});
@ -489,15 +498,15 @@ export function Selector<T>(props: {
Array.isArray(props.defaultSelectedValue)
? props.defaultSelectedValue
: props.defaultSelectedValue !== undefined
? [props.defaultSelectedValue]
: [],
? [props.defaultSelectedValue]
: [],
);
const handleSelection = (e: MouseEvent, value: T) => {
if (props.multiple) {
e.stopPropagation();
const newSelectedValues = selectedValues.includes(value)
? selectedValues.filter((v) => v !== value)
? selectedValues.filter(v => v !== value)
: [...selectedValues, value];
setSelectedValues(newSelectedValues);
props.onSelection?.(newSelectedValues);
@ -509,15 +518,15 @@ export function Selector<T>(props: {
};
return (
<div className={styles["selector"]} onClick={() => props.onClose?.()}>
<div className={styles["selector-content"]}>
<div className={styles.selector} onClick={() => props.onClose?.()}>
<div className={styles['selector-content']}>
<List>
{props.items.map((item, i) => {
const selected = selectedValues.includes(item.value);
return (
<ListItem
className={clsx(styles["selector-item"], {
[styles["selector-item-disabled"]]: item.disable,
className={clsx(styles['selector-item'], {
[styles['selector-item-disabled']]: item.disable,
})}
key={i}
title={item.title}
@ -530,18 +539,21 @@ export function Selector<T>(props: {
}
}}
>
{selected ? (
<div
style={{
height: 10,
width: 10,
backgroundColor: "var(--primary)",
borderRadius: 10,
}}
></div>
) : (
<></>
)}
{selected
? (
<div
style={{
height: 10,
width: 10,
backgroundColor: 'var(--primary)',
borderRadius: 10,
}}
>
</div>
)
: (
<></>
)}
</ListItem>
);
})}
@ -567,14 +579,14 @@ export function FullScreen(props: any) {
setFullScreen(!!document.fullscreenElement);
}
};
document.addEventListener("fullscreenchange", handleScreenChange);
document.addEventListener('fullscreenchange', handleScreenChange);
return () => {
document.removeEventListener("fullscreenchange", handleScreenChange);
document.removeEventListener('fullscreenchange', handleScreenChange);
};
}, []);
return (
<div ref={ref} style={{ position: "relative" }} {...rest}>
<div style={{ position: "absolute", right, top }}>
<div ref={ref} style={{ position: 'relative' }} {...rest}>
<div style={{ position: 'absolute', right, top }}>
<IconButton
icon={fullScreen ? <MinIcon /> : <MaxIcon />}
onClick={toggleFullscreen}

View File

@ -1 +1 @@
export * from "./voice-print";
export * from './voice-print';

View File

@ -1,5 +1,5 @@
import { useEffect, useRef, useCallback } from "react";
import styles from "./voice-print.module.scss";
import { useCallback, useEffect, useRef } from 'react';
import styles from './voice-print.module.scss';
interface VoicePrintProps {
frequencies?: Uint8Array;
@ -29,10 +29,12 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
if (!canvas)
{ return; }
const ctx = canvas.getContext("2d");
if (!ctx) return;
const ctx = canvas.getContext('2d');
if (!ctx)
{ return; }
/**
* DPI屏幕显示
@ -92,10 +94,10 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
* 3.
*/
if (historyRef.current.length > 0) {
const historicalValues = historyRef.current.map((h) => h[i] || 0);
avgFrequency =
(avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) /
(historyRef.current.length + 1);
const historicalValues = historyRef.current.map(h => h[i] || 0);
avgFrequency
= (avgFrequency + historicalValues.reduce((a, b) => a + b, 0))
/ (historyRef.current.length + 1);
}
/**
@ -151,9 +153,9 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
* 使
*/
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)");
gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)");
gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)");
gradient.addColorStop(0, 'rgba(100, 180, 255, 0.95)');
gradient.addColorStop(0.5, 'rgba(140, 200, 255, 0.9)');
gradient.addColorStop(1, 'rgba(180, 220, 255, 0.95)');
ctx.fillStyle = gradient;
ctx.fill();
@ -173,7 +175,7 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
}, [frequencies, isActive, updateHistory]);
return (
<div className={styles["voice-print"]}>
<div className={styles['voice-print']}>
<canvas ref={canvasRef} />
</div>
);

View File

@ -1,20 +1,20 @@
import packageJson from "../../package.json";
import { DEFAULT_INPUT_TEMPLATE } from "../constant";
import packageJson from '../../package.json';
import { DEFAULT_INPUT_TEMPLATE } from '../constant';
export const getBuildConfig = () => {
if (typeof process === "undefined") {
throw Error(
"[Server Config] you are importing a nodejs-only module outside of nodejs",
export function getBuildConfig() {
if (typeof process === 'undefined') {
throw new TypeError(
'[Server Config] you are importing a nodejs-only module outside of nodejs',
);
}
const buildMode = process.env.BUILD_MODE ?? "standalone";
const buildMode = process.env.BUILD_MODE ?? 'standalone';
const isApp = !!process.env.BUILD_APP;
const version = "v" + packageJson.version;
const version = `v${packageJson.version}`;
const commitInfo = (() => {
try {
const childProcess = require("child_process");
const childProcess = require('node:child_process');
const commitDate: string = childProcess
.execSync('git log -1 --format="%at000" --date=unix')
.toString()
@ -26,10 +26,10 @@ export const getBuildConfig = () => {
return { commitDate, commitHash };
} catch (e) {
console.error("[Build Config] No git or not from git repo.");
console.error('[Build Config] No git or not from git repo.');
return {
commitDate: "unknown",
commitHash: "unknown",
commitDate: 'unknown',
commitHash: 'unknown',
};
}
})();
@ -41,6 +41,6 @@ export const getBuildConfig = () => {
isApp,
template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE,
};
};
}
export type BuildConfig = ReturnType<typeof getBuildConfig>;

View File

@ -1,12 +1,13 @@
import { BuildConfig, getBuildConfig } from "./build";
import type { BuildConfig } from './build';
import { getBuildConfig } from './build';
export function getClientConfig() {
if (typeof document !== "undefined") {
if (typeof document !== 'undefined') {
// client side
return JSON.parse(queryMeta("config") || "{}") as BuildConfig;
return JSON.parse(queryMeta('config') || '{}') as BuildConfig;
}
if (typeof process !== "undefined") {
if (typeof process !== 'undefined') {
// server side
return getBuildConfig();
}
@ -18,9 +19,9 @@ function queryMeta(key: string, defaultValue?: string): string {
const meta = document.head.querySelector(
`meta[name='${key}']`,
) as HTMLMetaElement;
ret = meta?.content ?? "";
ret = meta?.content ?? '';
} else {
ret = defaultValue ?? "";
ret = defaultValue ?? '';
}
return ret;

View File

@ -1,5 +1,5 @@
import md5 from "spark-md5";
import { DEFAULT_MODELS, DEFAULT_GA_ID } from "../constant";
import md5 from 'spark-md5';
import { DEFAULT_GA_ID, DEFAULT_MODELS } from '../constant';
declare global {
namespace NodeJS {
@ -13,7 +13,7 @@ declare global {
OPENAI_ORG_ID?: string; // openai only
VERCEL?: string;
BUILD_MODE?: "standalone" | "export";
BUILD_MODE?: 'standalone' | 'export';
BUILD_APP?: string; // is building desktop app
HIDE_USER_API_KEY?: string; // disable user's api key input
@ -89,9 +89,9 @@ const ACCESS_CODES = (function getAccessCodes(): Set<string> {
const code = process.env.CODE;
try {
const codes = (code?.split(",") ?? [])
.filter((v) => !!v)
.map((v) => md5.hash(v.trim()));
const codes = (code?.split(',') ?? [])
.filter(v => !!v)
.map(v => md5.hash(v.trim()));
return new Set(codes);
} catch (e) {
return new Set();
@ -99,8 +99,8 @@ const ACCESS_CODES = (function getAccessCodes(): Set<string> {
})();
function getApiKey(keys?: string) {
const apiKeyEnvVar = keys ?? "";
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
const apiKeyEnvVar = keys ?? '';
const apiKeys = apiKeyEnvVar.split(',').map(v => v.trim());
const randomIndex = Math.floor(Math.random() * apiKeys.length);
const apiKey = apiKeys[randomIndex];
if (apiKey) {
@ -114,33 +114,35 @@ function getApiKey(keys?: string) {
return apiKey;
}
export const getServerSideConfig = () => {
if (typeof process === "undefined") {
throw Error(
"[Server Config] you are importing a nodejs-only module outside of nodejs",
export function getServerSideConfig() {
if (typeof process === 'undefined') {
throw new TypeError(
'[Server Config] you are importing a nodejs-only module outside of nodejs',
);
}
const disableGPT4 = !!process.env.DISABLE_GPT4;
let customModels = process.env.CUSTOM_MODELS ?? "";
let defaultModel = process.env.DEFAULT_MODEL ?? "";
let customModels = process.env.CUSTOM_MODELS ?? '';
let defaultModel = process.env.DEFAULT_MODEL ?? '';
if (disableGPT4) {
if (customModels) customModels += ",";
if (customModels)
{ customModels += ','; }
customModels += DEFAULT_MODELS.filter(
(m) =>
(m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o") || m.name.startsWith("o1")) &&
!m.name.startsWith("gpt-4o-mini"),
m =>
(m.name.startsWith('gpt-4') || m.name.startsWith('chatgpt-4o') || m.name.startsWith('o1'))
&& !m.name.startsWith('gpt-4o-mini'),
)
.map((m) => "-" + m.name)
.join(",");
.map(m => `-${m.name}`)
.join(',');
if (
(defaultModel.startsWith("gpt-4") ||
defaultModel.startsWith("chatgpt-4o") ||
defaultModel.startsWith("o1")) &&
!defaultModel.startsWith("gpt-4o-mini")
)
defaultModel = "";
(defaultModel.startsWith('gpt-4')
|| defaultModel.startsWith('chatgpt-4o')
|| defaultModel.startsWith('o1'))
&& !defaultModel.startsWith('gpt-4o-mini')
) {
defaultModel = '';
}
}
const isStability = !!process.env.STABILITY_API_KEY;
@ -166,8 +168,8 @@ export const getServerSideConfig = () => {
// );
const allowedWebDavEndpoints = (
process.env.WHITE_WEBDAV_ENDPOINTS ?? ""
).split(",");
process.env.WHITE_WEBDAV_ENDPOINTS ?? ''
).split(',');
return {
baseUrl: process.env.BASE_URL,
@ -250,4 +252,4 @@ export const getServerSideConfig = () => {
defaultModel,
allowedWebDavEndpoints,
};
};
}

View File

@ -1,5 +1,5 @@
export const OWNER = "ChatGPTNextWeb";
export const REPO = "ChatGPT-Next-Web";
export const OWNER = 'ChatGPTNextWeb';
export const REPO = 'ChatGPT-Next-Web';
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
export const PLUGINS_REPO_URL = `https://github.com/${OWNER}/NextChat-Awesome-Plugins`;
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
@ -7,86 +7,86 @@ export const UPDATE_URL = `${REPO_URL}#keep-updated`;
export const RELEASE_URL = `${REPO_URL}/releases`;
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
export const RUNTIME_CONFIG_DOM = 'danger-runtime-config';
export const STABILITY_BASE_URL = "https://api.stability.ai";
export const STABILITY_BASE_URL = 'https://api.stability.ai';
export const OPENAI_BASE_URL = "https://api.openai.com";
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
export const OPENAI_BASE_URL = 'https://api.openai.com';
export const ANTHROPIC_BASE_URL = 'https://api.anthropic.com';
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
export const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/';
export const BAIDU_BASE_URL = "https://aip.baidubce.com";
export const BAIDU_BASE_URL = 'https://aip.baidubce.com';
export const BAIDU_OATUH_URL = `${BAIDU_BASE_URL}/oauth/2.0/token`;
export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
export const BYTEDANCE_BASE_URL = 'https://ark.cn-beijing.volces.com';
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
export const ALIBABA_BASE_URL = 'https://dashscope.aliyuncs.com/api/';
export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
export const TENCENT_BASE_URL = 'https://hunyuan.tencentcloudapi.com';
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
export const MOONSHOT_BASE_URL = 'https://api.moonshot.cn';
export const IFLYTEK_BASE_URL = 'https://spark-api-open.xf-yun.com';
export const XAI_BASE_URL = "https://api.x.ai";
export const XAI_BASE_URL = 'https://api.x.ai';
export const CHATGLM_BASE_URL = "https://open.bigmodel.cn";
export const CHATGLM_BASE_URL = 'https://open.bigmodel.cn';
export const CACHE_URL_PREFIX = "/api/cache";
export const CACHE_URL_PREFIX = '/api/cache';
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
export enum Path {
Home = "/",
Chat = "/chat",
Settings = "/settings",
NewChat = "/new-chat",
Masks = "/masks",
Plugins = "/plugins",
Auth = "/auth",
Sd = "/sd",
SdNew = "/sd-new",
Artifacts = "/artifacts",
SearchChat = "/search-chat",
Home = '/',
Chat = '/chat',
Settings = '/settings',
NewChat = '/new-chat',
Masks = '/masks',
Plugins = '/plugins',
Auth = '/auth',
Sd = '/sd',
SdNew = '/sd-new',
Artifacts = '/artifacts',
SearchChat = '/search-chat',
}
export enum ApiPath {
Cors = "",
Azure = "/api/azure",
OpenAI = "/api/openai",
Anthropic = "/api/anthropic",
Google = "/api/google",
Baidu = "/api/baidu",
ByteDance = "/api/bytedance",
Alibaba = "/api/alibaba",
Tencent = "/api/tencent",
Moonshot = "/api/moonshot",
Iflytek = "/api/iflytek",
Stability = "/api/stability",
Artifacts = "/api/artifacts",
XAI = "/api/xai",
ChatGLM = "/api/chatglm",
Cors = '',
Azure = '/api/azure',
OpenAI = '/api/openai',
Anthropic = '/api/anthropic',
Google = '/api/google',
Baidu = '/api/baidu',
ByteDance = '/api/bytedance',
Alibaba = '/api/alibaba',
Tencent = '/api/tencent',
Moonshot = '/api/moonshot',
Iflytek = '/api/iflytek',
Stability = '/api/stability',
Artifacts = '/api/artifacts',
XAI = '/api/xai',
ChatGLM = '/api/chatglm',
}
export enum SlotID {
AppBody = "app-body",
CustomModel = "custom-model",
AppBody = 'app-body',
CustomModel = 'custom-model',
}
export enum FileName {
Masks = "masks.json",
Prompts = "prompts.json",
Masks = 'masks.json',
Prompts = 'prompts.json',
}
export enum StoreKey {
Chat = "chat-next-web-store",
Plugin = "chat-next-web-plugin",
Access = "access-control",
Config = "app-config",
Mask = "mask-store",
Prompt = "prompt-store",
Update = "chat-update",
Sync = "sync",
SdList = "sd-list",
Chat = 'chat-next-web-store',
Plugin = 'chat-next-web-plugin',
Access = 'access-control',
Config = 'app-config',
Mask = 'mask-store',
Prompt = 'prompt-store',
Update = 'chat-update',
Sync = 'sync',
SdList = 'sd-list',
}
export const DEFAULT_SIDEBAR_WIDTH = 300;
@ -94,76 +94,76 @@ export const MAX_SIDEBAR_WIDTH = 500;
export const MIN_SIDEBAR_WIDTH = 230;
export const NARROW_SIDEBAR_WIDTH = 100;
export const ACCESS_CODE_PREFIX = "nk-";
export const ACCESS_CODE_PREFIX = 'nk-';
export const LAST_INPUT_KEY = "last-input";
export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id;
export const LAST_INPUT_KEY = 'last-input';
export const UNFINISHED_INPUT = (id: string) => `unfinished-input-${id}`;
export const STORAGE_KEY = "chatgpt-next-web";
export const STORAGE_KEY = 'chatgpt-next-web';
export const REQUEST_TIMEOUT_MS = 60000;
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
export const EXPORT_MESSAGE_CLASS_NAME = 'export-markdown';
export enum ServiceProvider {
OpenAI = "OpenAI",
Azure = "Azure",
Google = "Google",
Anthropic = "Anthropic",
Baidu = "Baidu",
ByteDance = "ByteDance",
Alibaba = "Alibaba",
Tencent = "Tencent",
Moonshot = "Moonshot",
Stability = "Stability",
Iflytek = "Iflytek",
XAI = "XAI",
ChatGLM = "ChatGLM",
OpenAI = 'OpenAI',
Azure = 'Azure',
Google = 'Google',
Anthropic = 'Anthropic',
Baidu = 'Baidu',
ByteDance = 'ByteDance',
Alibaba = 'Alibaba',
Tencent = 'Tencent',
Moonshot = 'Moonshot',
Stability = 'Stability',
Iflytek = 'Iflytek',
XAI = 'XAI',
ChatGLM = 'ChatGLM',
}
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
// BLOCK_NONE will not block any content, and BLOCK_ONLY_HIGH will block only high-risk content.
export enum GoogleSafetySettingsThreshold {
BLOCK_NONE = "BLOCK_NONE",
BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH",
BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE",
BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE",
BLOCK_NONE = 'BLOCK_NONE',
BLOCK_ONLY_HIGH = 'BLOCK_ONLY_HIGH',
BLOCK_MEDIUM_AND_ABOVE = 'BLOCK_MEDIUM_AND_ABOVE',
BLOCK_LOW_AND_ABOVE = 'BLOCK_LOW_AND_ABOVE',
}
export enum ModelProvider {
Stability = "Stability",
GPT = "GPT",
GeminiPro = "GeminiPro",
Claude = "Claude",
Ernie = "Ernie",
Doubao = "Doubao",
Qwen = "Qwen",
Hunyuan = "Hunyuan",
Moonshot = "Moonshot",
Iflytek = "Iflytek",
XAI = "XAI",
ChatGLM = "ChatGLM",
Stability = 'Stability',
GPT = 'GPT',
GeminiPro = 'GeminiPro',
Claude = 'Claude',
Ernie = 'Ernie',
Doubao = 'Doubao',
Qwen = 'Qwen',
Hunyuan = 'Hunyuan',
Moonshot = 'Moonshot',
Iflytek = 'Iflytek',
XAI = 'XAI',
ChatGLM = 'ChatGLM',
}
export const Stability = {
GeneratePath: "v2beta/stable-image/generate",
ExampleEndpoint: "https://api.stability.ai",
GeneratePath: 'v2beta/stable-image/generate',
ExampleEndpoint: 'https://api.stability.ai',
};
export const Anthropic = {
ChatPath: "v1/messages",
ChatPath1: "v1/complete",
ExampleEndpoint: "https://api.anthropic.com",
Vision: "2023-06-01",
ChatPath: 'v1/messages',
ChatPath1: 'v1/complete',
ExampleEndpoint: 'https://api.anthropic.com',
Vision: '2023-06-01',
};
export const OpenaiPath = {
ChatPath: "v1/chat/completions",
SpeechPath: "v1/audio/speech",
ImagePath: "v1/images/generations",
UsagePath: "dashboard/billing/usage",
SubsPath: "dashboard/billing/subscription",
ListModelPath: "v1/models",
ChatPath: 'v1/chat/completions',
SpeechPath: 'v1/audio/speech',
ImagePath: 'v1/images/generations',
UsagePath: 'dashboard/billing/usage',
SubsPath: 'dashboard/billing/subscription',
ListModelPath: 'v1/models',
};
export const Azure = {
@ -172,11 +172,11 @@ export const Azure = {
// https://<your_resource_name>.openai.azure.com/openai/deployments/<your_deployment_name>/images/generations?api-version=<api_version>
ImagePath: (deployName: string, apiVersion: string) =>
`deployments/${deployName}/images/generations?api-version=${apiVersion}`,
ExampleEndpoint: "https://{resource-url}/openai",
ExampleEndpoint: 'https://{resource-url}/openai',
};
export const Google = {
ExampleEndpoint: "https://generativelanguage.googleapis.com/",
ExampleEndpoint: 'https://generativelanguage.googleapis.com/',
ChatPath: (modelName: string) =>
`v1beta/models/${modelName}:streamGenerateContent`,
};
@ -185,30 +185,30 @@ export const Baidu = {
ExampleEndpoint: BAIDU_BASE_URL,
ChatPath: (modelName: string) => {
let endpoint = modelName;
if (modelName === "ernie-4.0-8k") {
endpoint = "completions_pro";
if (modelName === 'ernie-4.0-8k') {
endpoint = 'completions_pro';
}
if (modelName === "ernie-4.0-8k-preview-0518") {
endpoint = "completions_adv_pro";
if (modelName === 'ernie-4.0-8k-preview-0518') {
endpoint = 'completions_adv_pro';
}
if (modelName === "ernie-3.5-8k") {
endpoint = "completions";
if (modelName === 'ernie-3.5-8k') {
endpoint = 'completions';
}
if (modelName === "ernie-speed-8k") {
endpoint = "ernie_speed";
if (modelName === 'ernie-speed-8k') {
endpoint = 'ernie_speed';
}
return `rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`;
},
};
export const ByteDance = {
ExampleEndpoint: "https://ark.cn-beijing.volces.com/api/",
ChatPath: "api/v3/chat/completions",
ExampleEndpoint: 'https://ark.cn-beijing.volces.com/api/',
ChatPath: 'api/v3/chat/completions',
};
export const Alibaba = {
ExampleEndpoint: ALIBABA_BASE_URL,
ChatPath: "v1/services/aigc/text-generation/generation",
ChatPath: 'v1/services/aigc/text-generation/generation',
};
export const Tencent = {
@ -217,22 +217,22 @@ export const Tencent = {
export const Moonshot = {
ExampleEndpoint: MOONSHOT_BASE_URL,
ChatPath: "v1/chat/completions",
ChatPath: 'v1/chat/completions',
};
export const Iflytek = {
ExampleEndpoint: IFLYTEK_BASE_URL,
ChatPath: "v1/chat/completions",
ChatPath: 'v1/chat/completions',
};
export const XAI = {
ExampleEndpoint: XAI_BASE_URL,
ChatPath: "v1/chat/completions",
ChatPath: 'v1/chat/completions',
};
export const ChatGLM = {
ExampleEndpoint: CHATGLM_BASE_URL,
ChatPath: "api/paas/v4/chat/completions",
ChatPath: 'api/paas/v4/chat/completions',
};
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
@ -253,291 +253,291 @@ Latex inline: \\(x^2\\)
Latex block: $$e=mc^2$$
`;
export const SUMMARIZE_MODEL = "gpt-4o-mini";
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
export const SUMMARIZE_MODEL = 'gpt-4o-mini';
export const GEMINI_SUMMARIZE_MODEL = 'gemini-pro';
export const KnowledgeCutOffDate: Record<string, string> = {
default: "2021-09",
"gpt-4-turbo": "2023-12",
"gpt-4-turbo-2024-04-09": "2023-12",
"gpt-4-turbo-preview": "2023-12",
"gpt-4o": "2023-10",
"gpt-4o-2024-05-13": "2023-10",
"gpt-4o-2024-08-06": "2023-10",
"gpt-4o-2024-11-20": "2023-10",
"chatgpt-4o-latest": "2023-10",
"gpt-4o-mini": "2023-10",
"gpt-4o-mini-2024-07-18": "2023-10",
"gpt-4-vision-preview": "2023-04",
"o1-mini": "2023-10",
"o1-preview": "2023-10",
'default': '2021-09',
'gpt-4-turbo': '2023-12',
'gpt-4-turbo-2024-04-09': '2023-12',
'gpt-4-turbo-preview': '2023-12',
'gpt-4o': '2023-10',
'gpt-4o-2024-05-13': '2023-10',
'gpt-4o-2024-08-06': '2023-10',
'gpt-4o-2024-11-20': '2023-10',
'chatgpt-4o-latest': '2023-10',
'gpt-4o-mini': '2023-10',
'gpt-4o-mini-2024-07-18': '2023-10',
'gpt-4-vision-preview': '2023-04',
'o1-mini': '2023-10',
'o1-preview': '2023-10',
// After improvements,
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
"gemini-pro": "2023-12",
"gemini-pro-vision": "2023-12",
'gemini-pro': '2023-12',
'gemini-pro-vision': '2023-12',
};
export const DEFAULT_TTS_ENGINE = "OpenAI-TTS";
export const DEFAULT_TTS_ENGINES = ["OpenAI-TTS", "Edge-TTS"];
export const DEFAULT_TTS_MODEL = "tts-1";
export const DEFAULT_TTS_VOICE = "alloy";
export const DEFAULT_TTS_MODELS = ["tts-1", "tts-1-hd"];
export const DEFAULT_TTS_ENGINE = 'OpenAI-TTS';
export const DEFAULT_TTS_ENGINES = ['OpenAI-TTS', 'Edge-TTS'];
export const DEFAULT_TTS_MODEL = 'tts-1';
export const DEFAULT_TTS_VOICE = 'alloy';
export const DEFAULT_TTS_MODELS = ['tts-1', 'tts-1-hd'];
export const DEFAULT_TTS_VOICES = [
"alloy",
"echo",
"fable",
"onyx",
"nova",
"shimmer",
'alloy',
'echo',
'fable',
'onyx',
'nova',
'shimmer',
];
const openaiModels = [
"gpt-3.5-turbo",
"gpt-3.5-turbo-1106",
"gpt-3.5-turbo-0125",
"gpt-4",
"gpt-4-0613",
"gpt-4-32k",
"gpt-4-32k-0613",
"gpt-4-turbo",
"gpt-4-turbo-preview",
"gpt-4o",
"gpt-4o-2024-05-13",
"gpt-4o-2024-08-06",
"gpt-4o-2024-11-20",
"chatgpt-4o-latest",
"gpt-4o-mini",
"gpt-4o-mini-2024-07-18",
"gpt-4-vision-preview",
"gpt-4-turbo-2024-04-09",
"gpt-4-1106-preview",
"dall-e-3",
"o1-mini",
"o1-preview",
'gpt-3.5-turbo',
'gpt-3.5-turbo-1106',
'gpt-3.5-turbo-0125',
'gpt-4',
'gpt-4-0613',
'gpt-4-32k',
'gpt-4-32k-0613',
'gpt-4-turbo',
'gpt-4-turbo-preview',
'gpt-4o',
'gpt-4o-2024-05-13',
'gpt-4o-2024-08-06',
'gpt-4o-2024-11-20',
'chatgpt-4o-latest',
'gpt-4o-mini',
'gpt-4o-mini-2024-07-18',
'gpt-4-vision-preview',
'gpt-4-turbo-2024-04-09',
'gpt-4-1106-preview',
'dall-e-3',
'o1-mini',
'o1-preview',
];
const googleModels = [
"gemini-1.0-pro",
"gemini-1.5-pro-latest",
"gemini-1.5-flash-latest",
"gemini-exp-1114",
"gemini-exp-1121",
"learnlm-1.5-pro-experimental",
"gemini-pro-vision",
'gemini-1.0-pro',
'gemini-1.5-pro-latest',
'gemini-1.5-flash-latest',
'gemini-exp-1114',
'gemini-exp-1121',
'learnlm-1.5-pro-experimental',
'gemini-pro-vision',
];
const anthropicModels = [
"claude-instant-1.2",
"claude-2.0",
"claude-2.1",
"claude-3-sonnet-20240229",
"claude-3-opus-20240229",
"claude-3-opus-latest",
"claude-3-haiku-20240307",
"claude-3-5-haiku-20241022",
"claude-3-5-haiku-latest",
"claude-3-5-sonnet-20240620",
"claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-latest",
'claude-instant-1.2',
'claude-2.0',
'claude-2.1',
'claude-3-sonnet-20240229',
'claude-3-opus-20240229',
'claude-3-opus-latest',
'claude-3-haiku-20240307',
'claude-3-5-haiku-20241022',
'claude-3-5-haiku-latest',
'claude-3-5-sonnet-20240620',
'claude-3-5-sonnet-20241022',
'claude-3-5-sonnet-latest',
];
const baiduModels = [
"ernie-4.0-turbo-8k",
"ernie-4.0-8k",
"ernie-4.0-8k-preview",
"ernie-4.0-8k-preview-0518",
"ernie-4.0-8k-latest",
"ernie-3.5-8k",
"ernie-3.5-8k-0205",
"ernie-speed-128k",
"ernie-speed-8k",
"ernie-lite-8k",
"ernie-tiny-8k",
'ernie-4.0-turbo-8k',
'ernie-4.0-8k',
'ernie-4.0-8k-preview',
'ernie-4.0-8k-preview-0518',
'ernie-4.0-8k-latest',
'ernie-3.5-8k',
'ernie-3.5-8k-0205',
'ernie-speed-128k',
'ernie-speed-8k',
'ernie-lite-8k',
'ernie-tiny-8k',
];
const bytedanceModels = [
"Doubao-lite-4k",
"Doubao-lite-32k",
"Doubao-lite-128k",
"Doubao-pro-4k",
"Doubao-pro-32k",
"Doubao-pro-128k",
'Doubao-lite-4k',
'Doubao-lite-32k',
'Doubao-lite-128k',
'Doubao-pro-4k',
'Doubao-pro-32k',
'Doubao-pro-128k',
];
const alibabaModes = [
"qwen-turbo",
"qwen-plus",
"qwen-max",
"qwen-max-0428",
"qwen-max-0403",
"qwen-max-0107",
"qwen-max-longcontext",
'qwen-turbo',
'qwen-plus',
'qwen-max',
'qwen-max-0428',
'qwen-max-0403',
'qwen-max-0107',
'qwen-max-longcontext',
];
const tencentModels = [
"hunyuan-pro",
"hunyuan-standard",
"hunyuan-lite",
"hunyuan-role",
"hunyuan-functioncall",
"hunyuan-code",
"hunyuan-vision",
'hunyuan-pro',
'hunyuan-standard',
'hunyuan-lite',
'hunyuan-role',
'hunyuan-functioncall',
'hunyuan-code',
'hunyuan-vision',
];
const moonshotModes = ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"];
const moonshotModes = ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'];
const iflytekModels = [
"general",
"generalv3",
"pro-128k",
"generalv3.5",
"4.0Ultra",
'general',
'generalv3',
'pro-128k',
'generalv3.5',
'4.0Ultra',
];
const xAIModes = ["grok-beta"];
const xAIModes = ['grok-beta'];
const chatglmModels = [
"glm-4-plus",
"glm-4-0520",
"glm-4",
"glm-4-air",
"glm-4-airx",
"glm-4-long",
"glm-4-flashx",
"glm-4-flash",
'glm-4-plus',
'glm-4-0520',
'glm-4',
'glm-4-air',
'glm-4-airx',
'glm-4-long',
'glm-4-flashx',
'glm-4-flash',
];
let seq = 1000; // 内置的模型序号生成器从1000开始
export const DEFAULT_MODELS = [
...openaiModels.map((name) => ({
...openaiModels.map(name => ({
name,
available: true,
sorted: seq++, // Global sequence sort(index)
provider: {
id: "openai",
providerName: "OpenAI",
providerType: "openai",
id: 'openai',
providerName: 'OpenAI',
providerType: 'openai',
sorted: 1, // 这里是固定的,确保顺序与之前内置的版本一致
},
})),
...openaiModels.map((name) => ({
...openaiModels.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "azure",
providerName: "Azure",
providerType: "azure",
id: 'azure',
providerName: 'Azure',
providerType: 'azure',
sorted: 2,
},
})),
...googleModels.map((name) => ({
...googleModels.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "google",
providerName: "Google",
providerType: "google",
id: 'google',
providerName: 'Google',
providerType: 'google',
sorted: 3,
},
})),
...anthropicModels.map((name) => ({
...anthropicModels.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "anthropic",
providerName: "Anthropic",
providerType: "anthropic",
id: 'anthropic',
providerName: 'Anthropic',
providerType: 'anthropic',
sorted: 4,
},
})),
...baiduModels.map((name) => ({
...baiduModels.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "baidu",
providerName: "Baidu",
providerType: "baidu",
id: 'baidu',
providerName: 'Baidu',
providerType: 'baidu',
sorted: 5,
},
})),
...bytedanceModels.map((name) => ({
...bytedanceModels.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "bytedance",
providerName: "ByteDance",
providerType: "bytedance",
id: 'bytedance',
providerName: 'ByteDance',
providerType: 'bytedance',
sorted: 6,
},
})),
...alibabaModes.map((name) => ({
...alibabaModes.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "alibaba",
providerName: "Alibaba",
providerType: "alibaba",
id: 'alibaba',
providerName: 'Alibaba',
providerType: 'alibaba',
sorted: 7,
},
})),
...tencentModels.map((name) => ({
...tencentModels.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "tencent",
providerName: "Tencent",
providerType: "tencent",
id: 'tencent',
providerName: 'Tencent',
providerType: 'tencent',
sorted: 8,
},
})),
...moonshotModes.map((name) => ({
...moonshotModes.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "moonshot",
providerName: "Moonshot",
providerType: "moonshot",
id: 'moonshot',
providerName: 'Moonshot',
providerType: 'moonshot',
sorted: 9,
},
})),
...iflytekModels.map((name) => ({
...iflytekModels.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "iflytek",
providerName: "Iflytek",
providerType: "iflytek",
id: 'iflytek',
providerName: 'Iflytek',
providerType: 'iflytek',
sorted: 10,
},
})),
...xAIModes.map((name) => ({
...xAIModes.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "xai",
providerName: "XAI",
providerType: "xai",
id: 'xai',
providerName: 'XAI',
providerType: 'xai',
sorted: 11,
},
})),
...chatglmModels.map((name) => ({
...chatglmModels.map(name => ({
name,
available: true,
sorted: seq++,
provider: {
id: "chatglm",
providerName: "ChatGLM",
providerType: "chatglm",
id: 'chatglm',
providerName: 'ChatGLM',
providerType: 'chatglm',
sorted: 12,
},
})),
@ -548,23 +548,23 @@ export const MAX_RENDER_MSG_COUNT = 45;
// some famous webdav endpoints
export const internalAllowedWebDavEndpoints = [
"https://dav.jianguoyun.com/dav/",
"https://dav.dropdav.com/",
"https://dav.box.com/dav",
"https://nanao.teracloud.jp/dav/",
"https://bora.teracloud.jp/dav/",
"https://webdav.4shared.com/",
"https://dav.idrivesync.com",
"https://webdav.yandex.com",
"https://app.koofr.net/dav/Koofr",
'https://dav.jianguoyun.com/dav/',
'https://dav.dropdav.com/',
'https://dav.box.com/dav',
'https://nanao.teracloud.jp/dav/',
'https://bora.teracloud.jp/dav/',
'https://webdav.4shared.com/',
'https://dav.idrivesync.com',
'https://webdav.yandex.com',
'https://app.koofr.net/dav/Koofr',
];
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
export const DEFAULT_GA_ID = 'G-89WN60ZK2E';
export const PLUGINS = [
{ name: "Plugins", path: Path.Plugins },
{ name: "Stable Diffusion", path: Path.Sd },
{ name: "Search Chat", path: Path.SearchChat },
{ name: 'Plugins', path: Path.Plugins },
{ name: 'Stable Diffusion', path: Path.Sd },
{ name: 'Search Chat', path: Path.SearchChat },
];
export const SAAS_CHAT_URL = "https://nextchat.dev/chat";
export const SAAS_CHAT_UTM_URL = "https://nextchat.dev/chat?utm=github";
export const SAAS_CHAT_URL = 'https://nextchat.dev/chat';
export const SAAS_CHAT_UTM_URL = 'https://nextchat.dev/chat?utm=github';

14
app/global.d.ts vendored
View File

@ -1,13 +1,13 @@
declare module "*.jpg";
declare module "*.png";
declare module "*.woff2";
declare module "*.woff";
declare module "*.ttf";
declare module "*.scss" {
declare module '*.jpg';
declare module '*.png';
declare module '*.woff2';
declare module '*.woff';
declare module '*.ttf';
declare module '*.scss' {
const content: Record<string, string>;
export default content;
}
declare module "*.svg";
declare module '*.svg';
declare interface Window {}

View File

@ -1,30 +1,30 @@
/* eslint-disable @next/next/no-page-custom-font */
import "./styles/globals.scss";
import "./styles/markdown.scss";
import "./styles/highlight.scss";
import { getClientConfig } from "./config/client";
import type { Metadata, Viewport } from "next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { getServerSideConfig } from "./config/server";
import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google";
import type { Metadata, Viewport } from 'next';
import { GoogleAnalytics, GoogleTagManager } from '@next/third-parties/google';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { getClientConfig } from './config/client';
import { getServerSideConfig } from './config/server';
import './styles/globals.scss';
import './styles/markdown.scss';
import './styles/highlight.scss';
const serverConfig = getServerSideConfig();
export const metadata: Metadata = {
title: "NextChat",
description: "Your personal ChatGPT Chat Bot.",
title: 'NextChat',
description: 'Your personal ChatGPT Chat Bot.',
appleWebApp: {
title: "NextChat",
statusBarStyle: "default",
title: 'NextChat',
statusBarStyle: 'default',
},
};
export const viewport: Viewport = {
width: "device-width",
width: 'device-width',
initialScale: 1,
maximumScale: 1,
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#fafafa" },
{ media: "(prefers-color-scheme: dark)", color: "#151515" },
{ media: '(prefers-color-scheme: light)', color: '#fafafa' },
{ media: '(prefers-color-scheme: dark)', color: '#151515' },
],
};
@ -45,7 +45,8 @@ export default function RootLayout({
rel="manifest"
href="/site.webmanifest"
crossOrigin="use-credentials"
></link>
>
</link>
<script src="/serviceWorkerRegister.js" defer></script>
</head>
<body>

View File

@ -29,7 +29,7 @@ export class AudioHandler {
}
async initialize() {
await this.context.audioWorklet.addModule("/audio-processor.js");
await this.context.audioWorklet.addModule('/audio-processor.js');
}
async startRecording(onChunk: (chunk: Uint8Array) => void) {
@ -51,17 +51,17 @@ export class AudioHandler {
this.source = this.context.createMediaStreamSource(this.stream);
this.workletNode = new AudioWorkletNode(
this.context,
"audio-recorder-processor",
'audio-recorder-processor',
);
this.workletNode.port.onmessage = (event) => {
if (event.data.eventType === "audio") {
if (event.data.eventType === 'audio') {
const float32Data = event.data.audioData;
const int16Data = new Int16Array(float32Data.length);
for (let i = 0; i < float32Data.length; i++) {
const s = Math.max(-1, Math.min(1, float32Data[i]));
int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
const uint8Data = new Uint8Array(int16Data.buffer);
@ -76,24 +76,25 @@ export class AudioHandler {
this.source.connect(this.mergeNode, 0, 0);
this.workletNode.connect(this.context.destination);
this.workletNode.port.postMessage({ command: "START_RECORDING" });
this.workletNode.port.postMessage({ command: 'START_RECORDING' });
} catch (error) {
console.error("Error starting recording:", error);
console.error('Error starting recording:', error);
throw error;
}
}
stopRecording() {
if (!this.workletNode || !this.source || !this.stream) {
throw new Error("Recording not started");
throw new Error('Recording not started');
}
this.workletNode.port.postMessage({ command: "STOP_RECORDING" });
this.workletNode.port.postMessage({ command: 'STOP_RECORDING' });
this.workletNode.disconnect();
this.source.disconnect();
this.stream.getTracks().forEach((track) => track.stop());
this.stream.getTracks().forEach(track => track.stop());
}
startStreamingPlayback() {
this.isPlaying = true;
this.nextPlayTime = this.context.currentTime;
@ -101,13 +102,14 @@ export class AudioHandler {
stopStreamingPlayback() {
this.isPlaying = false;
this.playbackQueue.forEach((source) => source.stop());
this.playbackQueue.forEach(source => source.stop());
this.playbackQueue = [];
this.playBuffer = [];
}
playChunk(chunk: Uint8Array) {
if (!this.isPlaying) return;
if (!this.isPlaying)
{ return; }
const int16Data = new Int16Array(chunk.buffer);
// @ts-ignore
@ -115,7 +117,7 @@ export class AudioHandler {
const float32Data = new Float32Array(int16Data.length);
for (let i = 0; i < int16Data.length; i++) {
float32Data[i] = int16Data[i] / (int16Data[i] < 0 ? 0x8000 : 0x7fff);
float32Data[i] = int16Data[i] / (int16Data[i] < 0 ? 0x8000 : 0x7FFF);
}
const audioBuffer = this.context.createBuffer(
@ -148,6 +150,7 @@ export class AudioHandler {
this.nextPlayTime = this.context.currentTime;
}
}
_saveData(data: Int16Array, bytesPerSample = 16): Blob {
const headerLength = 44;
const numberOfChannels = 1;
@ -169,12 +172,15 @@ export class AudioHandler {
view.setUint32(40, byteLength, true); // data chunk length
// using data.buffer, so no need to setUint16 to view.
return new Blob([view, data.buffer], { type: "audio/mpeg" });
// @ts-ignore
return new Blob([view, data.buffer], { type: 'audio/mpeg' });
}
savePlayFile() {
// @ts-ignore
return this._saveData(new Int16Array(this.playBuffer));
}
saveRecordFile(
audioStartMillis: number | undefined,
audioEndMillis: number | undefined,
@ -190,11 +196,12 @@ export class AudioHandler {
new Int16Array(this.recordBuffer.slice(startIndex, endIndex)),
);
}
async close() {
this.recordBuffer = [];
this.workletNode?.disconnect();
this.source?.disconnect();
this.stream?.getTracks().forEach((track) => track.stop());
this.stream?.getTracks().forEach(track => track.stop());
await this.context.close();
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,11 +1,12 @@
import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
import type { PartialLocaleType } from './index';
import { SAAS_CHAT_UTM_URL } from '@/app/constant';
import { getClientConfig } from '../config/client';
import { SubmitKey } from '../store/config';
const isApp = !!getClientConfig()?.isApp;
const id: PartialLocaleType = {
WIP: "Coming Soon...",
WIP: 'Coming Soon...',
Error: {
Unauthorized: isApp
? `😆 Percakapan mengalami beberapa masalah, tidak perlu khawatir:
@ -18,16 +19,16 @@ const id: PartialLocaleType = {
`,
},
Auth: {
Title: "Kebutuhan Kata Sandi",
Tips: "Administrator telah mengaktifkan verifikasi kata sandi, silakan masukkan kode akses di bawah ini",
SubTips: "Atau masukkan kunci API OpenAI atau Google Anda",
Input: "Masukkan kode akses di sini",
Confirm: "Konfirmasi",
Later: "Nanti",
Return: "Kembali",
SaasTips: "Konfigurasi terlalu rumit, saya ingin menggunakannya segera",
Title: 'Kebutuhan Kata Sandi',
Tips: 'Administrator telah mengaktifkan verifikasi kata sandi, silakan masukkan kode akses di bawah ini',
SubTips: 'Atau masukkan kunci API OpenAI atau Google Anda',
Input: 'Masukkan kode akses di sini',
Confirm: 'Konfirmasi',
Later: 'Nanti',
Return: 'Kembali',
SaasTips: 'Konfigurasi terlalu rumit, saya ingin menggunakannya segera',
TopTips:
"🥳 Penawaran Peluncuran NextChat AI, buka OpenAI o1, GPT-4o, Claude-3.5 dan model besar terbaru sekarang",
'🥳 Penawaran Peluncuran NextChat AI, buka OpenAI o1, GPT-4o, Claude-3.5 dan model besar terbaru sekarang',
},
ChatItem: {
ChatItemCount: (count: number) => `${count} percakapan`,
@ -35,560 +36,560 @@ const id: PartialLocaleType = {
Chat: {
SubTitle: (count: number) => `Total ${count} percakapan`,
EditMessage: {
Title: "Edit Riwayat Pesan",
Title: 'Edit Riwayat Pesan',
Topic: {
Title: "Topik Obrolan",
SubTitle: "Ubah topik obrolan saat ini",
Title: 'Topik Obrolan',
SubTitle: 'Ubah topik obrolan saat ini',
},
},
Actions: {
ChatList: "Lihat daftar pesan",
CompressedHistory: "Lihat riwayat Prompt yang dikompresi",
Export: "Ekspor riwayat obrolan",
Copy: "Salin",
Stop: "Berhenti",
Retry: "Coba lagi",
Pin: "Sematkan",
PinToastContent: "1 percakapan telah disematkan ke prompt default",
PinToastAction: "Lihat",
Delete: "Hapus",
Edit: "Edit",
RefreshTitle: "Segarkan Judul",
RefreshToast: "Permintaan penyegaran judul telah dikirim",
ChatList: 'Lihat daftar pesan',
CompressedHistory: 'Lihat riwayat Prompt yang dikompresi',
Export: 'Ekspor riwayat obrolan',
Copy: 'Salin',
Stop: 'Berhenti',
Retry: 'Coba lagi',
Pin: 'Sematkan',
PinToastContent: '1 percakapan telah disematkan ke prompt default',
PinToastAction: 'Lihat',
Delete: 'Hapus',
Edit: 'Edit',
RefreshTitle: 'Segarkan Judul',
RefreshToast: 'Permintaan penyegaran judul telah dikirim',
},
Commands: {
new: "Obrolan Baru",
newm: "Buat Obrolan Baru dari Masker",
next: "Obrolan Berikutnya",
prev: "Obrolan Sebelumnya",
clear: "Hapus Konteks",
del: "Hapus Obrolan",
new: 'Obrolan Baru',
newm: 'Buat Obrolan Baru dari Masker',
next: 'Obrolan Berikutnya',
prev: 'Obrolan Sebelumnya',
clear: 'Hapus Konteks',
del: 'Hapus Obrolan',
},
InputActions: {
Stop: "Hentikan Respons",
ToBottom: "Gulir ke bawah",
Stop: 'Hentikan Respons',
ToBottom: 'Gulir ke bawah',
Theme: {
auto: "Tema Otomatis",
light: "Mode Terang",
dark: "Mode Gelap",
auto: 'Tema Otomatis',
light: 'Mode Terang',
dark: 'Mode Gelap',
},
Prompt: "Perintah Cepat",
Masks: "Semua Masker",
Clear: "Hapus Obrolan",
Settings: "Pengaturan Obrolan",
UploadImage: "Unggah Gambar",
Prompt: 'Perintah Cepat',
Masks: 'Semua Masker',
Clear: 'Hapus Obrolan',
Settings: 'Pengaturan Obrolan',
UploadImage: 'Unggah Gambar',
},
Rename: "Ganti Nama Obrolan",
Typing: "Sedang Mengetik…",
Rename: 'Ganti Nama Obrolan',
Typing: 'Sedang Mengetik…',
Input: (submitKey: string) => {
var inputHints = `${submitKey} kirim`;
let inputHints = `${submitKey} kirim`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter untuk baris baru";
inputHints += 'Shift + Enter untuk baris baru';
}
return inputHints + "/ untuk melengkapi, : untuk memicu perintah";
return `${inputHints}/ untuk melengkapi, : untuk memicu perintah`;
},
Send: "Kirim",
Send: 'Kirim',
Config: {
Reset: "Hapus Memori",
SaveAs: "Simpan sebagai Masker",
Reset: 'Hapus Memori',
SaveAs: 'Simpan sebagai Masker',
},
IsContext: "Prompt Default",
IsContext: 'Prompt Default',
},
Export: {
Title: "Bagikan Riwayat Obrolan",
Copy: "Salin Semua",
Download: "Unduh File",
Share: "Bagikan ke ShareGPT",
MessageFromYou: "Pengguna",
MessageFromChatGPT: "ChatGPT",
Title: 'Bagikan Riwayat Obrolan',
Copy: 'Salin Semua',
Download: 'Unduh File',
Share: 'Bagikan ke ShareGPT',
MessageFromYou: 'Pengguna',
MessageFromChatGPT: 'ChatGPT',
Format: {
Title: "Format Ekspor",
SubTitle: "Dapat mengekspor teks Markdown atau gambar PNG",
Title: 'Format Ekspor',
SubTitle: 'Dapat mengekspor teks Markdown atau gambar PNG',
},
IncludeContext: {
Title: "Sertakan Konteks Masker",
SubTitle: "Apakah akan menampilkan konteks masker dalam pesan",
Title: 'Sertakan Konteks Masker',
SubTitle: 'Apakah akan menampilkan konteks masker dalam pesan',
},
Steps: {
Select: "Pilih",
Preview: "Prabaca",
Select: 'Pilih',
Preview: 'Prabaca',
},
Image: {
Toast: "Sedang Membuat Screenshot",
Modal: "Tekan lama atau klik kanan untuk menyimpan gambar",
Toast: 'Sedang Membuat Screenshot',
Modal: 'Tekan lama atau klik kanan untuk menyimpan gambar',
},
},
Select: {
Search: "Cari Pesan",
All: "Pilih Semua",
Latest: "Beberapa Terbaru",
Clear: "Hapus Pilihan",
Search: 'Cari Pesan',
All: 'Pilih Semua',
Latest: 'Beberapa Terbaru',
Clear: 'Hapus Pilihan',
},
Memory: {
Title: "Ringkasan Sejarah",
EmptyContent: "Isi percakapan terlalu pendek, tidak perlu dirangkum",
Send: "Otomatis kompres riwayat obrolan dan kirim sebagai konteks",
Copy: "Salin Ringkasan",
Reset: "[unused]",
ResetConfirm: "Konfirmasi untuk menghapus ringkasan sejarah?",
Title: 'Ringkasan Sejarah',
EmptyContent: 'Isi percakapan terlalu pendek, tidak perlu dirangkum',
Send: 'Otomatis kompres riwayat obrolan dan kirim sebagai konteks',
Copy: 'Salin Ringkasan',
Reset: '[unused]',
ResetConfirm: 'Konfirmasi untuk menghapus ringkasan sejarah?',
},
Home: {
NewChat: "Obrolan Baru",
DeleteChat: "Konfirmasi untuk menghapus percakapan yang dipilih?",
DeleteToast: "Percakapan telah dihapus",
Revert: "Batalkan",
NewChat: 'Obrolan Baru',
DeleteChat: 'Konfirmasi untuk menghapus percakapan yang dipilih?',
DeleteToast: 'Percakapan telah dihapus',
Revert: 'Batalkan',
},
Settings: {
Title: "Pengaturan",
SubTitle: "Semua opsi pengaturan",
Title: 'Pengaturan',
SubTitle: 'Semua opsi pengaturan',
Danger: {
Reset: {
Title: "Atur Ulang Semua Pengaturan",
SubTitle: "Atur ulang semua opsi pengaturan ke nilai default",
Action: "Atur Ulang Sekarang",
Confirm: "Konfirmasi untuk mengatur ulang semua pengaturan?",
Title: 'Atur Ulang Semua Pengaturan',
SubTitle: 'Atur ulang semua opsi pengaturan ke nilai default',
Action: 'Atur Ulang Sekarang',
Confirm: 'Konfirmasi untuk mengatur ulang semua pengaturan?',
},
Clear: {
Title: "Hapus Semua Data",
SubTitle: "Hapus semua data obrolan dan pengaturan",
Action: "Hapus Sekarang",
Title: 'Hapus Semua Data',
SubTitle: 'Hapus semua data obrolan dan pengaturan',
Action: 'Hapus Sekarang',
Confirm:
"Konfirmasi untuk menghapus semua data obrolan dan pengaturan?",
'Konfirmasi untuk menghapus semua data obrolan dan pengaturan?',
},
},
Lang: {
Name: "Language", // PERHATIAN: jika Anda ingin menambahkan terjemahan baru, harap jangan terjemahkan nilai ini, biarkan sebagai `Language`
All: "Semua Bahasa",
Name: 'Language', // PERHATIAN: jika Anda ingin menambahkan terjemahan baru, harap jangan terjemahkan nilai ini, biarkan sebagai `Language`
All: 'Semua Bahasa',
},
Avatar: "Avatar",
Avatar: 'Avatar',
FontSize: {
Title: "Ukuran Font",
SubTitle: "Ukuran font untuk konten obrolan",
Title: 'Ukuran Font',
SubTitle: 'Ukuran font untuk konten obrolan',
},
FontFamily: {
Title: "Font Obrolan",
Title: 'Font Obrolan',
SubTitle:
"Font dari konten obrolan, biarkan kosong untuk menerapkan font default global",
Placeholder: "Nama Font",
'Font dari konten obrolan, biarkan kosong untuk menerapkan font default global',
Placeholder: 'Nama Font',
},
InjectSystemPrompts: {
Title: "Suntikkan Pesan Sistem",
Title: 'Suntikkan Pesan Sistem',
SubTitle:
"Memaksa menambahkan pesan sistem simulasi ChatGPT di awal daftar pesan setiap permintaan",
'Memaksa menambahkan pesan sistem simulasi ChatGPT di awal daftar pesan setiap permintaan',
},
InputTemplate: {
Title: "Pra-pemrosesan Input Pengguna",
SubTitle: "Pesan terbaru pengguna akan diisi ke template ini",
Title: 'Pra-pemrosesan Input Pengguna',
SubTitle: 'Pesan terbaru pengguna akan diisi ke template ini',
},
Update: {
Version: (x: string) => `Versi Saat Ini: ${x}`,
IsLatest: "Sudah versi terbaru",
CheckUpdate: "Periksa Pembaruan",
IsChecking: "Sedang memeriksa pembaruan...",
IsLatest: 'Sudah versi terbaru',
CheckUpdate: 'Periksa Pembaruan',
IsChecking: 'Sedang memeriksa pembaruan...',
FoundUpdate: (x: string) => `Versi Baru Ditemukan: ${x}`,
GoToUpdate: "Pergi ke Pembaruan",
GoToUpdate: 'Pergi ke Pembaruan',
},
SendKey: "Kunci Kirim",
Theme: "Tema",
TightBorder: "Mode Tanpa Border",
SendKey: 'Kunci Kirim',
Theme: 'Tema',
TightBorder: 'Mode Tanpa Border',
SendPreviewBubble: {
Title: "Preview Bubble",
SubTitle: "Pratinjau konten Markdown di bubble pratinjau",
Title: 'Preview Bubble',
SubTitle: 'Pratinjau konten Markdown di bubble pratinjau',
},
AutoGenerateTitle: {
Title: "Otomatis Membuat Judul",
SubTitle: "Membuat judul yang sesuai berdasarkan konten obrolan",
Title: 'Otomatis Membuat Judul',
SubTitle: 'Membuat judul yang sesuai berdasarkan konten obrolan',
},
Sync: {
CloudState: "Data Cloud",
NotSyncYet: "Belum disinkronkan",
Success: "Sinkronisasi Berhasil",
Fail: "Sinkronisasi Gagal",
CloudState: 'Data Cloud',
NotSyncYet: 'Belum disinkronkan',
Success: 'Sinkronisasi Berhasil',
Fail: 'Sinkronisasi Gagal',
Config: {
Modal: {
Title: "Konfigurasi Sinkronisasi Cloud",
Check: "Periksa Ketersediaan",
Title: 'Konfigurasi Sinkronisasi Cloud',
Check: 'Periksa Ketersediaan',
},
SyncType: {
Title: "Jenis Sinkronisasi",
SubTitle: "Pilih server sinkronisasi favorit",
Title: 'Jenis Sinkronisasi',
SubTitle: 'Pilih server sinkronisasi favorit',
},
Proxy: {
Title: "Aktifkan Proxy",
Title: 'Aktifkan Proxy',
SubTitle:
"Saat menyinkronkan di browser, proxy harus diaktifkan untuk menghindari pembatasan lintas domain",
'Saat menyinkronkan di browser, proxy harus diaktifkan untuk menghindari pembatasan lintas domain',
},
ProxyUrl: {
Title: "Alamat Proxy",
SubTitle: "Hanya berlaku untuk proxy lintas domain bawaan proyek ini",
Title: 'Alamat Proxy',
SubTitle: 'Hanya berlaku untuk proxy lintas domain bawaan proyek ini',
},
WebDav: {
Endpoint: "Alamat WebDAV",
UserName: "Nama Pengguna",
Password: "Kata Sandi",
Endpoint: 'Alamat WebDAV',
UserName: 'Nama Pengguna',
Password: 'Kata Sandi',
},
UpStash: {
Endpoint: "Url REST Redis UpStash",
UserName: "Nama Cadangan",
Password: "Token REST Redis UpStash",
Endpoint: 'Url REST Redis UpStash',
UserName: 'Nama Cadangan',
Password: 'Token REST Redis UpStash',
},
},
LocalState: "Data Lokal",
LocalState: 'Data Lokal',
Overview: (overview: any) => {
return `${overview.chat} percakapan, ${overview.message} pesan, ${overview.prompt} prompt, ${overview.mask} masker`;
},
ImportFailed: "Impor Gagal",
ImportFailed: 'Impor Gagal',
},
Mask: {
Splash: {
Title: "Halaman Awal Masker",
SubTitle: "Tampilkan halaman awal masker saat memulai obrolan baru",
Title: 'Halaman Awal Masker',
SubTitle: 'Tampilkan halaman awal masker saat memulai obrolan baru',
},
Builtin: {
Title: "Sembunyikan Masker Bawaan",
SubTitle: "Sembunyikan masker bawaan dari semua daftar masker",
Title: 'Sembunyikan Masker Bawaan',
SubTitle: 'Sembunyikan masker bawaan dari semua daftar masker',
},
},
Prompt: {
Disable: {
Title: "Nonaktifkan Pelengkapan Prompt Otomatis",
Title: 'Nonaktifkan Pelengkapan Prompt Otomatis',
SubTitle:
"Ketik / di awal kotak input untuk memicu pelengkapan otomatis",
'Ketik / di awal kotak input untuk memicu pelengkapan otomatis',
},
List: "Daftar Prompt Kustom",
List: 'Daftar Prompt Kustom',
ListCount: (builtin: number, custom: number) =>
`Bawaan ${builtin} item, pengguna ${custom} item`,
Edit: "Edit",
Edit: 'Edit',
Modal: {
Title: "Daftar Prompt",
Add: "Baru",
Search: "Cari Prompt",
Title: 'Daftar Prompt',
Add: 'Baru',
Search: 'Cari Prompt',
},
EditModal: {
Title: "Edit Prompt",
Title: 'Edit Prompt',
},
},
HistoryCount: {
Title: "Jumlah Pesan Sejarah",
SubTitle: "Jumlah pesan sejarah yang dibawa setiap permintaan",
Title: 'Jumlah Pesan Sejarah',
SubTitle: 'Jumlah pesan sejarah yang dibawa setiap permintaan',
},
CompressThreshold: {
Title: "Ambang Batas Kompresi Pesan Sejarah",
Title: 'Ambang Batas Kompresi Pesan Sejarah',
SubTitle:
"Ketika pesan sejarah yang tidak terkompresi melebihi nilai ini, akan dikompresi",
'Ketika pesan sejarah yang tidak terkompresi melebihi nilai ini, akan dikompresi',
},
Usage: {
Title: "Cek Saldo",
Title: 'Cek Saldo',
SubTitle(used: any, total: any) {
return `Digunakan bulan ini $${used}, total langganan $${total}`;
},
IsChecking: "Sedang memeriksa…",
Check: "Periksa Lagi",
NoAccess: "Masukkan API Key atau kata sandi akses untuk melihat saldo",
IsChecking: 'Sedang memeriksa…',
Check: 'Periksa Lagi',
NoAccess: 'Masukkan API Key atau kata sandi akses untuk melihat saldo',
},
Access: {
SaasStart: {
Title: "Gunakan NextChat AI",
Label: "(Solusi paling hemat biaya)",
Title: 'Gunakan NextChat AI',
Label: '(Solusi paling hemat biaya)',
SubTitle:
"Dikelola secara resmi oleh NextChat, siap digunakan tanpa konfigurasi, mendukung model besar terbaru seperti OpenAI o1, GPT-4o, dan Claude-3.5",
ChatNow: "Chat Sekarang",
'Dikelola secara resmi oleh NextChat, siap digunakan tanpa konfigurasi, mendukung model besar terbaru seperti OpenAI o1, GPT-4o, dan Claude-3.5',
ChatNow: 'Chat Sekarang',
},
AccessCode: {
Title: "Kata Sandi Akses",
SubTitle: "Administrator telah mengaktifkan akses terenkripsi",
Placeholder: "Masukkan kata sandi akses",
Title: 'Kata Sandi Akses',
SubTitle: 'Administrator telah mengaktifkan akses terenkripsi',
Placeholder: 'Masukkan kata sandi akses',
},
CustomEndpoint: {
Title: "Antarmuka Kustom",
SubTitle: "Apakah akan menggunakan layanan Azure atau OpenAI kustom",
Title: 'Antarmuka Kustom',
SubTitle: 'Apakah akan menggunakan layanan Azure atau OpenAI kustom',
},
Provider: {
Title: "Penyedia Layanan Model",
SubTitle: "Ganti penyedia layanan yang berbeda",
Title: 'Penyedia Layanan Model',
SubTitle: 'Ganti penyedia layanan yang berbeda',
},
OpenAI: {
ApiKey: {
Title: "API Key",
Title: 'API Key',
SubTitle:
"Gunakan OpenAI Key kustom untuk menghindari batasan akses kata sandi",
Placeholder: "OpenAI API Key",
'Gunakan OpenAI Key kustom untuk menghindari batasan akses kata sandi',
Placeholder: 'OpenAI API Key',
},
Endpoint: {
Title: "Alamat Antarmuka",
SubTitle: "Selain alamat default, harus menyertakan http(s)://",
Title: 'Alamat Antarmuka',
SubTitle: 'Selain alamat default, harus menyertakan http(s)://',
},
},
Azure: {
ApiKey: {
Title: "Kunci Antarmuka",
Title: 'Kunci Antarmuka',
SubTitle:
"Gunakan Azure Key kustom untuk menghindari batasan akses kata sandi",
Placeholder: "Azure API Key",
'Gunakan Azure Key kustom untuk menghindari batasan akses kata sandi',
Placeholder: 'Azure API Key',
},
Endpoint: {
Title: "Alamat Antarmuka",
SubTitle: "Contoh:",
Title: 'Alamat Antarmuka',
SubTitle: 'Contoh:',
},
ApiVerion: {
Title: "Versi Antarmuka (azure api version)",
SubTitle: "Pilih versi parsial tertentu",
Title: 'Versi Antarmuka (azure api version)',
SubTitle: 'Pilih versi parsial tertentu',
},
},
Anthropic: {
ApiKey: {
Title: "Kunci Antarmuka",
Title: 'Kunci Antarmuka',
SubTitle:
"Gunakan Anthropic Key kustom untuk menghindari batasan akses kata sandi",
Placeholder: "Anthropic API Key",
'Gunakan Anthropic Key kustom untuk menghindari batasan akses kata sandi',
Placeholder: 'Anthropic API Key',
},
Endpoint: {
Title: "Alamat Antarmuka",
SubTitle: "Contoh:",
Title: 'Alamat Antarmuka',
SubTitle: 'Contoh:',
},
ApiVerion: {
Title: "Versi Antarmuka (claude api version)",
SubTitle: "Pilih versi API tertentu",
Title: 'Versi Antarmuka (claude api version)',
SubTitle: 'Pilih versi API tertentu',
},
},
Google: {
ApiKey: {
Title: "Kunci API",
SubTitle: "Dapatkan kunci API Anda dari Google AI",
Placeholder: "Masukkan kunci API Studio Google AI Anda",
Title: 'Kunci API',
SubTitle: 'Dapatkan kunci API Anda dari Google AI',
Placeholder: 'Masukkan kunci API Studio Google AI Anda',
},
Endpoint: {
Title: "Alamat Akhir",
SubTitle: "Contoh:",
Title: 'Alamat Akhir',
SubTitle: 'Contoh:',
},
ApiVersion: {
Title: "Versi API (hanya untuk gemini-pro)",
SubTitle: "Pilih versi API tertentu",
Title: 'Versi API (hanya untuk gemini-pro)',
SubTitle: 'Pilih versi API tertentu',
},
GoogleSafetySettings: {
Title: "Tingkat Filter Keamanan Google",
SubTitle: "Atur tingkat filter konten",
Title: 'Tingkat Filter Keamanan Google',
SubTitle: 'Atur tingkat filter konten',
},
},
Baidu: {
ApiKey: {
Title: "API Key",
SubTitle: "Gunakan Baidu API Key kustom",
Placeholder: "Baidu API Key",
Title: 'API Key',
SubTitle: 'Gunakan Baidu API Key kustom',
Placeholder: 'Baidu API Key',
},
SecretKey: {
Title: "Secret Key",
SubTitle: "Gunakan Baidu Secret Key kustom",
Placeholder: "Baidu Secret Key",
Title: 'Secret Key',
SubTitle: 'Gunakan Baidu Secret Key kustom',
Placeholder: 'Baidu Secret Key',
},
Endpoint: {
Title: "Alamat Antarmuka",
SubTitle: "Tidak mendukung kustom, pergi ke .env untuk konfigurasi",
Title: 'Alamat Antarmuka',
SubTitle: 'Tidak mendukung kustom, pergi ke .env untuk konfigurasi',
},
},
ByteDance: {
ApiKey: {
Title: "Kunci Antarmuka",
SubTitle: "Gunakan ByteDance API Key kustom",
Placeholder: "ByteDance API Key",
Title: 'Kunci Antarmuka',
SubTitle: 'Gunakan ByteDance API Key kustom',
Placeholder: 'ByteDance API Key',
},
Endpoint: {
Title: "Alamat Antarmuka",
SubTitle: "Contoh:",
Title: 'Alamat Antarmuka',
SubTitle: 'Contoh:',
},
},
Alibaba: {
ApiKey: {
Title: "Kunci Antarmuka",
SubTitle: "Gunakan Alibaba Cloud API Key kustom",
Placeholder: "Alibaba Cloud API Key",
Title: 'Kunci Antarmuka',
SubTitle: 'Gunakan Alibaba Cloud API Key kustom',
Placeholder: 'Alibaba Cloud API Key',
},
Endpoint: {
Title: "Alamat Antarmuka",
SubTitle: "Contoh:",
Title: 'Alamat Antarmuka',
SubTitle: 'Contoh:',
},
},
CustomModel: {
Title: "Nama Model Kustom",
SubTitle: "Tambahkan opsi model kustom, pisahkan dengan koma",
Title: 'Nama Model Kustom',
SubTitle: 'Tambahkan opsi model kustom, pisahkan dengan koma',
},
},
Model: "Model",
Model: 'Model',
CompressModel: {
Title: "Model Kompresi",
SubTitle: "Model yang digunakan untuk mengompres riwayat",
Title: 'Model Kompresi',
SubTitle: 'Model yang digunakan untuk mengompres riwayat',
},
Temperature: {
Title: "Randomness (temperature)",
SubTitle: "Semakin tinggi nilainya, semakin acak responsnya",
Title: 'Randomness (temperature)',
SubTitle: 'Semakin tinggi nilainya, semakin acak responsnya',
},
TopP: {
Title: "Sampling Inti (top_p)",
Title: 'Sampling Inti (top_p)',
SubTitle:
"Mirip dengan randomness, tetapi jangan ubah bersama randomness",
'Mirip dengan randomness, tetapi jangan ubah bersama randomness',
},
MaxTokens: {
Title: "Batas Token Per Respons",
SubTitle: "Jumlah token maksimum yang digunakan per interaksi",
Title: 'Batas Token Per Respons',
SubTitle: 'Jumlah token maksimum yang digunakan per interaksi',
},
PresencePenalty: {
Title: "Kedekatan Topik (presence_penalty)",
Title: 'Kedekatan Topik (presence_penalty)',
SubTitle:
"Semakin tinggi nilainya, semakin besar kemungkinan memperluas ke topik baru",
'Semakin tinggi nilainya, semakin besar kemungkinan memperluas ke topik baru',
},
FrequencyPenalty: {
Title: "Hukuman Frekuensi (frequency_penalty)",
Title: 'Hukuman Frekuensi (frequency_penalty)',
SubTitle:
"Semakin tinggi nilainya, semakin besar kemungkinan mengurangi kata-kata yang berulang",
'Semakin tinggi nilainya, semakin besar kemungkinan mengurangi kata-kata yang berulang',
},
},
Store: {
DefaultTopic: "Obrolan Baru",
BotHello: "Ada yang bisa saya bantu?",
Error: "Terjadi kesalahan, coba lagi nanti",
DefaultTopic: 'Obrolan Baru',
BotHello: 'Ada yang bisa saya bantu?',
Error: 'Terjadi kesalahan, coba lagi nanti',
Prompt: {
History: (content: string) =>
"Ini adalah ringkasan obrolan sebelumnya sebagai latar belakang: " +
content,
`Ini adalah ringkasan obrolan sebelumnya sebagai latar belakang: ${
content}`,
Topic:
"Gunakan empat hingga lima kata untuk langsung memberikan ringkasan topik kalimat ini, tanpa penjelasan, tanpa tanda baca, tanpa kata pengisi, tanpa teks tambahan, tanpa menebalkan. Jika tidak ada topik, langsung jawab 'Obrolan Santai'",
'Gunakan empat hingga lima kata untuk langsung memberikan ringkasan topik kalimat ini, tanpa penjelasan, tanpa tanda baca, tanpa kata pengisi, tanpa teks tambahan, tanpa menebalkan. Jika tidak ada topik, langsung jawab \'Obrolan Santai\'',
Summarize:
"Berikan ringkasan singkat tentang konten obrolan, untuk digunakan sebagai prompt konteks selanjutnya, dalam 200 kata atau kurang",
'Berikan ringkasan singkat tentang konten obrolan, untuk digunakan sebagai prompt konteks selanjutnya, dalam 200 kata atau kurang',
},
},
Copy: {
Success: "Telah disalin ke clipboard",
Failed: "Gagal menyalin, mohon berikan izin clipboard",
Success: 'Telah disalin ke clipboard',
Failed: 'Gagal menyalin, mohon berikan izin clipboard',
},
Download: {
Success: "Konten telah diunduh ke direktori Anda.",
Failed: "Unduhan gagal.",
Success: 'Konten telah diunduh ke direktori Anda.',
Failed: 'Unduhan gagal.',
},
Context: {
Toast: (x: any) => `Berisi ${x} prompt preset`,
Edit: "Pengaturan Obrolan Saat Ini",
Add: "Tambah Obrolan",
Clear: "Konteks telah dihapus",
Revert: "Kembalikan Konteks",
Edit: 'Pengaturan Obrolan Saat Ini',
Add: 'Tambah Obrolan',
Clear: 'Konteks telah dihapus',
Revert: 'Kembalikan Konteks',
},
Plugin: {
Name: "Plugin",
Name: 'Plugin',
},
FineTuned: {
Sysmessage: "Anda adalah seorang asisten",
Sysmessage: 'Anda adalah seorang asisten',
},
SearchChat: {
Name: "Cari",
Name: 'Cari',
Page: {
Title: "Cari riwayat obrolan",
Search: "Masukkan kata kunci pencarian",
NoResult: "Tidak ada hasil ditemukan",
NoData: "Tidak ada data",
Loading: "Memuat",
Title: 'Cari riwayat obrolan',
Search: 'Masukkan kata kunci pencarian',
NoResult: 'Tidak ada hasil ditemukan',
NoData: 'Tidak ada data',
Loading: 'Memuat',
SubTitle: (count: number) => `Ditemukan ${count} hasil`,
},
Item: {
View: "Lihat",
View: 'Lihat',
},
},
Mask: {
Name: "Masker",
Name: 'Masker',
Page: {
Title: "Preset Karakter Masker",
Title: 'Preset Karakter Masker',
SubTitle: (count: number) => `${count} definisi karakter preset`,
Search: "Cari Masker Karakter",
Create: "Buat Baru",
Search: 'Cari Masker Karakter',
Create: 'Buat Baru',
},
Item: {
Info: (count: number) => `Berisi ${count} obrolan preset`,
Chat: "Obrolan",
View: "Lihat",
Edit: "Edit",
Delete: "Hapus",
DeleteConfirm: "Konfirmasi penghapusan?",
Chat: 'Obrolan',
View: 'Lihat',
Edit: 'Edit',
Delete: 'Hapus',
DeleteConfirm: 'Konfirmasi penghapusan?',
},
EditModal: {
Title: (readonly: boolean) =>
`Edit Masker Preset ${readonly ? "(Hanya Baca)" : ""}`,
Download: "Unduh Preset",
Clone: "Klon Preset",
`Edit Masker Preset ${readonly ? '(Hanya Baca)' : ''}`,
Download: 'Unduh Preset',
Clone: 'Klon Preset',
},
Config: {
Avatar: "Avatar Karakter",
Name: "Nama Karakter",
Avatar: 'Avatar Karakter',
Name: 'Nama Karakter',
Sync: {
Title: "Gunakan Pengaturan Global",
Title: 'Gunakan Pengaturan Global',
SubTitle:
"Apakah obrolan saat ini akan menggunakan pengaturan model global?",
'Apakah obrolan saat ini akan menggunakan pengaturan model global?',
Confirm:
"Pengaturan kustom obrolan saat ini akan ditimpa secara otomatis, konfirmasi untuk mengaktifkan pengaturan global?",
'Pengaturan kustom obrolan saat ini akan ditimpa secara otomatis, konfirmasi untuk mengaktifkan pengaturan global?',
},
HideContext: {
Title: "Sembunyikan Obrolan Preset",
Title: 'Sembunyikan Obrolan Preset',
SubTitle:
"Setelah disembunyikan, obrolan preset tidak akan muncul di antarmuka obrolan",
'Setelah disembunyikan, obrolan preset tidak akan muncul di antarmuka obrolan',
},
Share: {
Title: "Bagikan Masker Ini",
SubTitle: "Hasilkan tautan langsung ke masker ini",
Action: "Salin Tautan",
Title: 'Bagikan Masker Ini',
SubTitle: 'Hasilkan tautan langsung ke masker ini',
Action: 'Salin Tautan',
},
},
},
NewChat: {
Return: "Kembali",
Skip: "Mulai Sekarang",
NotShow: "Jangan Tampilkan Lagi",
Return: 'Kembali',
Skip: 'Mulai Sekarang',
NotShow: 'Jangan Tampilkan Lagi',
ConfirmNoShow:
"Konfirmasi untuk menonaktifkan? Setelah dinonaktifkan, Anda dapat mengaktifkannya kembali kapan saja di pengaturan.",
Title: "Pilih Masker",
SubTitle: "Mulai sekarang, berinteraksi dengan pemikiran di balik masker",
More: "Lihat Semua",
'Konfirmasi untuk menonaktifkan? Setelah dinonaktifkan, Anda dapat mengaktifkannya kembali kapan saja di pengaturan.',
Title: 'Pilih Masker',
SubTitle: 'Mulai sekarang, berinteraksi dengan pemikiran di balik masker',
More: 'Lihat Semua',
},
URLCommand: {
Code: "Terdeteksi bahwa tautan sudah mengandung kode akses, apakah akan diisi secara otomatis?",
Code: 'Terdeteksi bahwa tautan sudah mengandung kode akses, apakah akan diisi secara otomatis?',
Settings:
"Terdeteksi bahwa tautan mengandung pengaturan preset, apakah akan diisi secara otomatis?",
'Terdeteksi bahwa tautan mengandung pengaturan preset, apakah akan diisi secara otomatis?',
},
UI: {
Confirm: "Konfirmasi",
Cancel: "Batal",
Close: "Tutup",
Create: "Buat Baru",
Edit: "Edit",
Export: "Ekspor",
Import: "Impor",
Sync: "Sinkronkan",
Config: "Konfigurasi",
Confirm: 'Konfirmasi',
Cancel: 'Batal',
Close: 'Tutup',
Create: 'Buat Baru',
Edit: 'Edit',
Export: 'Ekspor',
Import: 'Impor',
Sync: 'Sinkronkan',
Config: 'Konfigurasi',
},
Exporter: {
Description: {
Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan",
Title: 'Hanya pesan setelah menghapus konteks yang akan ditampilkan',
},
Model: "Model",
Messages: "Pesan",
Topic: "Topik",
Time: "Waktu",
Model: 'Model',
Messages: 'Pesan',
Topic: 'Topik',
Time: 'Waktu',
},
};

View File

@ -1,27 +1,28 @@
import cn from "./cn";
import en from "./en";
import pt from "./pt";
import tw from "./tw";
import id from "./id";
import fr from "./fr";
import es from "./es";
import it from "./it";
import tr from "./tr";
import jp from "./jp";
import de from "./de";
import vi from "./vi";
import ru from "./ru";
import no from "./no";
import cs from "./cs";
import ko from "./ko";
import ar from "./ar";
import bn from "./bn";
import sk from "./sk";
import { merge } from "../utils/merge";
import { safeLocalStorage } from "@/app/utils";
import type { LocaleType } from './cn';
import { safeLocalStorage } from '@/app/utils';
import { merge } from '../utils/merge';
import ar from './ar';
import bn from './bn';
import cn from './cn';
import cs from './cs';
import de from './de';
import en from './en';
import es from './es';
import fr from './fr';
import id from './id';
import it from './it';
import jp from './jp';
import ko from './ko';
import no from './no';
import pt from './pt';
import ru from './ru';
import sk from './sk';
import tr from './tr';
import tw from './tw';
import type { LocaleType } from "./cn";
export type { LocaleType, PartialLocaleType } from "./cn";
import vi from './vi';
export type { LocaleType, PartialLocaleType } from './cn';
const localStorage = safeLocalStorage();
@ -52,29 +53,29 @@ export type Lang = keyof typeof ALL_LANGS;
export const AllLangs = Object.keys(ALL_LANGS) as Lang[];
export const ALL_LANG_OPTIONS: Record<Lang, string> = {
cn: "简体中文",
en: "English",
pt: "Português",
tw: "繁體中文",
jp: "日本語",
ko: "한국어",
id: "Indonesia",
fr: "Français",
es: "Español",
it: "Italiano",
tr: "Türkçe",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
no: "Nynorsk",
ar: "العربية",
bn: "বাংলা",
sk: "Slovensky",
cn: '简体中文',
en: 'English',
pt: 'Português',
tw: '繁體中文',
jp: '日本語',
ko: '한국어',
id: 'Indonesia',
fr: 'Français',
es: 'Español',
it: 'Italiano',
tr: 'Türkçe',
de: 'Deutsch',
vi: 'Tiếng Việt',
ru: 'Русский',
cs: 'Čeština',
no: 'Nynorsk',
ar: 'العربية',
bn: 'বাংলা',
sk: 'Slovensky',
};
const LANG_KEY = "lang";
const DEFAULT_LANG = "en";
const LANG_KEY = 'lang';
const DEFAULT_LANG = 'en';
const fallbackLang = en;
const targetLang = ALL_LANGS[getLang()] as LocaleType;
@ -113,7 +114,7 @@ function getLanguage() {
export function getLang(): Lang {
const savedLang = getItem(LANG_KEY);
if (AllLangs.includes((savedLang ?? "") as Lang)) {
if (AllLangs.includes((savedLang ?? '') as Lang)) {
return savedLang as Lang;
}
@ -127,35 +128,35 @@ export function changeLang(lang: Lang) {
export function getISOLang() {
const isoLangString: Record<string, string> = {
cn: "zh-Hans",
tw: "zh-Hant",
cn: 'zh-Hans',
tw: 'zh-Hant',
};
const lang = getLang();
return isoLangString[lang] ?? lang;
}
const DEFAULT_STT_LANG = "zh-CN";
const DEFAULT_STT_LANG = 'zh-CN';
export const STT_LANG_MAP: Record<Lang, string> = {
cn: "zh-CN",
en: "en-US",
pt: "pt-BR",
tw: "zh-TW",
jp: "ja-JP",
ko: "ko-KR",
id: "id-ID",
fr: "fr-FR",
es: "es-ES",
it: "it-IT",
tr: "tr-TR",
de: "de-DE",
vi: "vi-VN",
ru: "ru-RU",
cs: "cs-CZ",
no: "no-NO",
ar: "ar-SA",
bn: "bn-BD",
sk: "sk-SK",
cn: 'zh-CN',
en: 'en-US',
pt: 'pt-BR',
tw: 'zh-TW',
jp: 'ja-JP',
ko: 'ko-KR',
id: 'id-ID',
fr: 'fr-FR',
es: 'es-ES',
it: 'it-IT',
tr: 'tr-TR',
de: 'de-DE',
vi: 'vi-VN',
ru: 'ru-RU',
cs: 'cs-CZ',
no: 'no-NO',
ar: 'ar-SA',
bn: 'bn-BD',
sk: 'sk-SK',
};
export function getSTTLang(): string {

View File

@ -1,11 +1,12 @@
import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
import type { PartialLocaleType } from './index';
import { SAAS_CHAT_UTM_URL } from '@/app/constant';
import { getClientConfig } from '../config/client';
import { SubmitKey } from '../store/config';
const isApp = !!getClientConfig()?.isApp;
const it: PartialLocaleType = {
WIP: "Work in progress...",
WIP: 'Work in progress...',
Error: {
Unauthorized: isApp
? `😆 La conversazione ha incontrato alcuni problemi, non preoccuparti:
@ -18,17 +19,17 @@ const it: PartialLocaleType = {
`,
},
Auth: {
Title: "Password richiesta",
Tips: "L'amministratore ha abilitato la verifica della password. Inserisci il codice di accesso qui sotto",
SubTips: "O inserisci la tua chiave API OpenAI o Google",
Input: "Inserisci il codice di accesso qui",
Confirm: "Conferma",
Later: "Più tardi",
Return: "Ritorna",
Title: 'Password richiesta',
Tips: 'L\'amministratore ha abilitato la verifica della password. Inserisci il codice di accesso qui sotto',
SubTips: 'O inserisci la tua chiave API OpenAI o Google',
Input: 'Inserisci il codice di accesso qui',
Confirm: 'Conferma',
Later: 'Più tardi',
Return: 'Ritorna',
SaasTips:
"La configurazione è troppo complicata, voglio usarlo immediatamente",
'La configurazione è troppo complicata, voglio usarlo immediatamente',
TopTips:
"🥳 Offerta di lancio NextChat AI, sblocca OpenAI o1, GPT-4o, Claude-3.5 e i più recenti modelli di grandi dimensioni",
'🥳 Offerta di lancio NextChat AI, sblocca OpenAI o1, GPT-4o, Claude-3.5 e i più recenti modelli di grandi dimensioni',
},
ChatItem: {
ChatItemCount: (count: number) => `${count} conversazioni`,
@ -36,572 +37,572 @@ const it: PartialLocaleType = {
Chat: {
SubTitle: (count: number) => `Totale ${count} conversazioni`,
EditMessage: {
Title: "Modifica cronologia messaggi",
Title: 'Modifica cronologia messaggi',
Topic: {
Title: "Argomento della chat",
SubTitle: "Modifica l'argomento della chat corrente",
Title: 'Argomento della chat',
SubTitle: 'Modifica l\'argomento della chat corrente',
},
},
Actions: {
ChatList: "Visualizza l'elenco dei messaggi",
CompressedHistory: "Visualizza la cronologia Prompt compressa",
Export: "Esporta la cronologia chat",
Copy: "Copia",
Stop: "Interrompi",
Retry: "Riprova",
Pin: "Fissa",
PinToastContent: "1 conversazione fissata ai suggerimenti predefiniti",
PinToastAction: "Visualizza",
Delete: "Elimina",
Edit: "Modifica",
RefreshTitle: "Aggiorna titolo",
RefreshToast: "Richiesta di aggiornamento del titolo inviata",
ChatList: 'Visualizza l\'elenco dei messaggi',
CompressedHistory: 'Visualizza la cronologia Prompt compressa',
Export: 'Esporta la cronologia chat',
Copy: 'Copia',
Stop: 'Interrompi',
Retry: 'Riprova',
Pin: 'Fissa',
PinToastContent: '1 conversazione fissata ai suggerimenti predefiniti',
PinToastAction: 'Visualizza',
Delete: 'Elimina',
Edit: 'Modifica',
RefreshTitle: 'Aggiorna titolo',
RefreshToast: 'Richiesta di aggiornamento del titolo inviata',
},
Commands: {
new: "Nuova chat",
newm: "Nuova chat da maschera",
next: "Chat successiva",
prev: "Chat precedente",
clear: "Pulisci contesto",
del: "Elimina chat",
new: 'Nuova chat',
newm: 'Nuova chat da maschera',
next: 'Chat successiva',
prev: 'Chat precedente',
clear: 'Pulisci contesto',
del: 'Elimina chat',
},
InputActions: {
Stop: "Interrompi risposta",
ToBottom: "Scorri fino al più recente",
Stop: 'Interrompi risposta',
ToBottom: 'Scorri fino al più recente',
Theme: {
auto: "Tema automatico",
light: "Tema chiaro",
dark: "Tema scuro",
auto: 'Tema automatico',
light: 'Tema chiaro',
dark: 'Tema scuro',
},
Prompt: "Comandi rapidi",
Masks: "Tutte le maschere",
Clear: "Pulisci chat",
Settings: "Impostazioni conversazione",
UploadImage: "Carica immagine",
Prompt: 'Comandi rapidi',
Masks: 'Tutte le maschere',
Clear: 'Pulisci chat',
Settings: 'Impostazioni conversazione',
UploadImage: 'Carica immagine',
},
Rename: "Rinomina conversazione",
Typing: "Digitazione in corso…",
Rename: 'Rinomina conversazione',
Typing: 'Digitazione in corso…',
Input: (submitKey: string) => {
var inputHints = `${submitKey} per inviare`;
let inputHints = `${submitKey} per inviare`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter per andare a capo";
inputHints += 'Shift + Enter per andare a capo';
}
return (
inputHints +
"/ per attivare il completamento automatico, : per attivare il comando"
`${inputHints
}/ per attivare il completamento automatico, : per attivare il comando`
);
},
Send: "Invia",
Send: 'Invia',
Config: {
Reset: "Pulisci memoria",
SaveAs: "Salva come maschera",
Reset: 'Pulisci memoria',
SaveAs: 'Salva come maschera',
},
IsContext: "Suggerimenti predefiniti",
IsContext: 'Suggerimenti predefiniti',
},
Export: {
Title: "Condividi cronologia chat",
Copy: "Copia tutto",
Download: "Scarica file",
Share: "Condividi su ShareGPT",
MessageFromYou: "Utente",
MessageFromChatGPT: "ChatGPT",
Title: 'Condividi cronologia chat',
Copy: 'Copia tutto',
Download: 'Scarica file',
Share: 'Condividi su ShareGPT',
MessageFromYou: 'Utente',
MessageFromChatGPT: 'ChatGPT',
Format: {
Title: "Formato di esportazione",
SubTitle: "Puoi esportare come testo Markdown o immagine PNG",
Title: 'Formato di esportazione',
SubTitle: 'Puoi esportare come testo Markdown o immagine PNG',
},
IncludeContext: {
Title: "Includi contesto maschera",
SubTitle: "Mostrare il contesto della maschera nei messaggi",
Title: 'Includi contesto maschera',
SubTitle: 'Mostrare il contesto della maschera nei messaggi',
},
Steps: {
Select: "Seleziona",
Preview: "Anteprima",
Select: 'Seleziona',
Preview: 'Anteprima',
},
Image: {
Toast: "Generazione dello screenshot in corso",
Toast: 'Generazione dello screenshot in corso',
Modal:
"Tieni premuto o fai clic con il tasto destro per salvare l'immagine",
'Tieni premuto o fai clic con il tasto destro per salvare l\'immagine',
},
},
Select: {
Search: "Cerca messaggi",
All: "Seleziona tutto",
Latest: "Ultimi messaggi",
Clear: "Pulisci selezione",
Search: 'Cerca messaggi',
All: 'Seleziona tutto',
Latest: 'Ultimi messaggi',
Clear: 'Pulisci selezione',
},
Memory: {
Title: "Riassunto storico",
Title: 'Riassunto storico',
EmptyContent:
"Il contenuto della conversazione è troppo breve, nessun riassunto necessario",
Send: "Comprimi automaticamente la cronologia chat e inviala come contesto",
Copy: "Copia riassunto",
Reset: "[unused]",
ResetConfirm: "Confermi la cancellazione del riassunto storico?",
'Il contenuto della conversazione è troppo breve, nessun riassunto necessario',
Send: 'Comprimi automaticamente la cronologia chat e inviala come contesto',
Copy: 'Copia riassunto',
Reset: '[unused]',
ResetConfirm: 'Confermi la cancellazione del riassunto storico?',
},
Home: {
NewChat: "Nuova chat",
DeleteChat: "Confermi l'eliminazione della conversazione selezionata?",
DeleteToast: "Conversazione eliminata",
Revert: "Annulla",
NewChat: 'Nuova chat',
DeleteChat: 'Confermi l\'eliminazione della conversazione selezionata?',
DeleteToast: 'Conversazione eliminata',
Revert: 'Annulla',
},
Settings: {
Title: "Impostazioni",
SubTitle: "Tutte le opzioni di impostazione",
Title: 'Impostazioni',
SubTitle: 'Tutte le opzioni di impostazione',
Danger: {
Reset: {
Title: "Ripristina tutte le impostazioni",
SubTitle: "Ripristina tutte le opzioni ai valori predefiniti",
Action: "Ripristina subito",
Confirm: "Confermi il ripristino di tutte le impostazioni?",
Title: 'Ripristina tutte le impostazioni',
SubTitle: 'Ripristina tutte le opzioni ai valori predefiniti',
Action: 'Ripristina subito',
Confirm: 'Confermi il ripristino di tutte le impostazioni?',
},
Clear: {
Title: "Elimina tutti i dati",
SubTitle: "Elimina tutte le chat e i dati delle impostazioni",
Action: "Elimina subito",
Title: 'Elimina tutti i dati',
SubTitle: 'Elimina tutte le chat e i dati delle impostazioni',
Action: 'Elimina subito',
Confirm:
"Confermi l'eliminazione di tutte le chat e dei dati delle impostazioni?",
'Confermi l\'eliminazione di tutte le chat e dei dati delle impostazioni?',
},
},
Lang: {
Name: "Language", // ATTENZIONE: se vuoi aggiungere una nuova traduzione, non tradurre questo valore, lascialo come `Language`
All: "Tutte le lingue",
Name: 'Language', // ATTENZIONE: se vuoi aggiungere una nuova traduzione, non tradurre questo valore, lascialo come `Language`
All: 'Tutte le lingue',
},
Avatar: "Avatar",
Avatar: 'Avatar',
FontSize: {
Title: "Dimensione del carattere",
SubTitle: "Dimensione del carattere per il contenuto della chat",
Title: 'Dimensione del carattere',
SubTitle: 'Dimensione del carattere per il contenuto della chat',
},
FontFamily: {
Title: "Font della Chat",
Title: 'Font della Chat',
SubTitle:
"Carattere del contenuto della chat, lascia vuoto per applicare il carattere predefinito globale",
Placeholder: "Nome del Font",
'Carattere del contenuto della chat, lascia vuoto per applicare il carattere predefinito globale',
Placeholder: 'Nome del Font',
},
InjectSystemPrompts: {
Title: "Inserisci suggerimenti di sistema",
Title: 'Inserisci suggerimenti di sistema',
SubTitle:
"Aggiungi forzatamente un suggerimento di sistema simulato di ChatGPT all'inizio della lista dei messaggi per ogni richiesta",
'Aggiungi forzatamente un suggerimento di sistema simulato di ChatGPT all\'inizio della lista dei messaggi per ogni richiesta',
},
InputTemplate: {
Title: "Preprocessing dell'input utente",
Title: 'Preprocessing dell\'input utente',
SubTitle:
"L'ultimo messaggio dell'utente verrà inserito in questo modello",
'L\'ultimo messaggio dell\'utente verrà inserito in questo modello',
},
Update: {
Version: (x: string) => `Versione attuale: ${x}`,
IsLatest: "È l'ultima versione",
CheckUpdate: "Controlla aggiornamenti",
IsChecking: "Verifica aggiornamenti in corso...",
IsLatest: 'È l\'ultima versione',
CheckUpdate: 'Controlla aggiornamenti',
IsChecking: 'Verifica aggiornamenti in corso...',
FoundUpdate: (x: string) => `Nuova versione trovata: ${x}`,
GoToUpdate: "Vai all'aggiornamento",
GoToUpdate: 'Vai all\'aggiornamento',
},
SendKey: "Tasto di invio",
Theme: "Tema",
TightBorder: "Modalità senza bordi",
SendKey: 'Tasto di invio',
Theme: 'Tema',
TightBorder: 'Modalità senza bordi',
SendPreviewBubble: {
Title: "Bolla di anteprima",
SubTitle: "Anteprima del contenuto Markdown nella bolla di anteprima",
Title: 'Bolla di anteprima',
SubTitle: 'Anteprima del contenuto Markdown nella bolla di anteprima',
},
AutoGenerateTitle: {
Title: "Generazione automatica del titolo",
Title: 'Generazione automatica del titolo',
SubTitle:
"Genera un titolo appropriato in base al contenuto della conversazione",
'Genera un titolo appropriato in base al contenuto della conversazione',
},
Sync: {
CloudState: "Dati cloud",
NotSyncYet: "Non è ancora avvenuta alcuna sincronizzazione",
Success: "Sincronizzazione riuscita",
Fail: "Sincronizzazione fallita",
CloudState: 'Dati cloud',
NotSyncYet: 'Non è ancora avvenuta alcuna sincronizzazione',
Success: 'Sincronizzazione riuscita',
Fail: 'Sincronizzazione fallita',
Config: {
Modal: {
Title: "Configura sincronizzazione cloud",
Check: "Controlla disponibilità",
Title: 'Configura sincronizzazione cloud',
Check: 'Controlla disponibilità',
},
SyncType: {
Title: "Tipo di sincronizzazione",
SubTitle: "Scegli il server di sincronizzazione preferito",
Title: 'Tipo di sincronizzazione',
SubTitle: 'Scegli il server di sincronizzazione preferito',
},
Proxy: {
Title: "Abilita proxy",
Title: 'Abilita proxy',
SubTitle:
"Durante la sincronizzazione nel browser, è necessario abilitare il proxy per evitare restrizioni CORS",
'Durante la sincronizzazione nel browser, è necessario abilitare il proxy per evitare restrizioni CORS',
},
ProxyUrl: {
Title: "Indirizzo proxy",
SubTitle: "Solo per il proxy CORS fornito con questo progetto",
Title: 'Indirizzo proxy',
SubTitle: 'Solo per il proxy CORS fornito con questo progetto',
},
WebDav: {
Endpoint: "Indirizzo WebDAV",
UserName: "Nome utente",
Password: "Password",
Endpoint: 'Indirizzo WebDAV',
UserName: 'Nome utente',
Password: 'Password',
},
UpStash: {
Endpoint: "URL REST di UpStash Redis",
UserName: "Nome di backup",
Password: "Token REST di UpStash Redis",
Endpoint: 'URL REST di UpStash Redis',
UserName: 'Nome di backup',
Password: 'Token REST di UpStash Redis',
},
},
LocalState: "Dati locali",
LocalState: 'Dati locali',
Overview: (overview: any) => {
return `${overview.chat} chat, ${overview.message} messaggi, ${overview.prompt} suggerimenti, ${overview.mask} maschere`;
},
ImportFailed: "Importazione fallita",
ImportFailed: 'Importazione fallita',
},
Mask: {
Splash: {
Title: "Pagina di avvio delle maschere",
Title: 'Pagina di avvio delle maschere',
SubTitle:
"Mostra la pagina di avvio delle maschere quando si avvia una nuova chat",
'Mostra la pagina di avvio delle maschere quando si avvia una nuova chat',
},
Builtin: {
Title: "Nascondi maschere predefinite",
Title: 'Nascondi maschere predefinite',
SubTitle:
"Nascondi le maschere predefinite in tutte le liste delle maschere",
'Nascondi le maschere predefinite in tutte le liste delle maschere',
},
},
Prompt: {
Disable: {
Title: "Disabilita completamento automatico dei suggerimenti",
Title: 'Disabilita completamento automatico dei suggerimenti',
SubTitle:
"Inserisci / all'inizio della casella di input per attivare il completamento automatico",
'Inserisci / all\'inizio della casella di input per attivare il completamento automatico',
},
List: "Elenco dei suggerimenti personalizzati",
List: 'Elenco dei suggerimenti personalizzati',
ListCount: (builtin: number, custom: number) =>
`${builtin} predefiniti, ${custom} definiti dall'utente`,
Edit: "Modifica",
Edit: 'Modifica',
Modal: {
Title: "Elenco dei suggerimenti",
Add: "Nuovo",
Search: "Cerca suggerimenti",
Title: 'Elenco dei suggerimenti',
Add: 'Nuovo',
Search: 'Cerca suggerimenti',
},
EditModal: {
Title: "Modifica suggerimenti",
Title: 'Modifica suggerimenti',
},
},
HistoryCount: {
Title: "Numero di messaggi storici inclusi",
SubTitle: "Numero di messaggi storici inclusi in ogni richiesta",
Title: 'Numero di messaggi storici inclusi',
SubTitle: 'Numero di messaggi storici inclusi in ogni richiesta',
},
CompressThreshold: {
Title: "Soglia di compressione dei messaggi storici",
Title: 'Soglia di compressione dei messaggi storici',
SubTitle:
"Quando i messaggi storici non compressi superano questo valore, verranno compressi",
'Quando i messaggi storici non compressi superano questo valore, verranno compressi',
},
Usage: {
Title: "Verifica saldo",
Title: 'Verifica saldo',
SubTitle(used: any, total: any) {
return `Utilizzato questo mese $${used}, totale abbonamento $${total}`;
},
IsChecking: "Verifica in corso…",
Check: "Verifica di nuovo",
IsChecking: 'Verifica in corso…',
Check: 'Verifica di nuovo',
NoAccess:
"Inserisci API Key o password di accesso per visualizzare il saldo",
'Inserisci API Key o password di accesso per visualizzare il saldo',
},
Access: {
SaasStart: {
Title: "Usa NextChat AI",
Label: "(La soluzione più conveniente)",
Title: 'Usa NextChat AI',
Label: '(La soluzione più conveniente)',
SubTitle:
"Mantenuto ufficialmente da NextChat, pronto all'uso senza configurazione, supporta i modelli più recenti come OpenAI o1, GPT-4o e Claude-3.5",
ChatNow: "Chatta ora",
'Mantenuto ufficialmente da NextChat, pronto all\'uso senza configurazione, supporta i modelli più recenti come OpenAI o1, GPT-4o e Claude-3.5',
ChatNow: 'Chatta ora',
},
AccessCode: {
Title: "Password di accesso",
SubTitle: "L'amministratore ha abilitato l'accesso criptato",
Placeholder: "Inserisci la password di accesso",
Title: 'Password di accesso',
SubTitle: 'L\'amministratore ha abilitato l\'accesso criptato',
Placeholder: 'Inserisci la password di accesso',
},
CustomEndpoint: {
Title: "Interfaccia personalizzata",
SubTitle: "Utilizzare servizi Azure o OpenAI personalizzati",
Title: 'Interfaccia personalizzata',
SubTitle: 'Utilizzare servizi Azure o OpenAI personalizzati',
},
Provider: {
Title: "Fornitore del modello",
SubTitle: "Cambia fornitore di servizi",
Title: 'Fornitore del modello',
SubTitle: 'Cambia fornitore di servizi',
},
OpenAI: {
ApiKey: {
Title: "API Key",
Title: 'API Key',
SubTitle:
"Utilizza una chiave OpenAI personalizzata per bypassare le limitazioni di accesso",
Placeholder: "API Key OpenAI",
'Utilizza una chiave OpenAI personalizzata per bypassare le limitazioni di accesso',
Placeholder: 'API Key OpenAI',
},
Endpoint: {
Title: "Indirizzo dell'interfaccia",
SubTitle: "Deve includere http(s):// oltre all'indirizzo predefinito",
Title: 'Indirizzo dell\'interfaccia',
SubTitle: 'Deve includere http(s):// oltre all\'indirizzo predefinito',
},
},
Azure: {
ApiKey: {
Title: "Chiave dell'interfaccia",
Title: 'Chiave dell\'interfaccia',
SubTitle:
"Utilizza una chiave Azure personalizzata per bypassare le limitazioni di accesso",
Placeholder: "Chiave API Azure",
'Utilizza una chiave Azure personalizzata per bypassare le limitazioni di accesso',
Placeholder: 'Chiave API Azure',
},
Endpoint: {
Title: "Indirizzo dell'interfaccia",
SubTitle: "Esempio:",
Title: 'Indirizzo dell\'interfaccia',
SubTitle: 'Esempio:',
},
ApiVerion: {
Title: "Versione dell'interfaccia (versione api azure)",
SubTitle: "Scegli una versione specifica",
Title: 'Versione dell\'interfaccia (versione api azure)',
SubTitle: 'Scegli una versione specifica',
},
},
Anthropic: {
ApiKey: {
Title: "Chiave dell'interfaccia",
Title: 'Chiave dell\'interfaccia',
SubTitle:
"Utilizza una chiave Anthropic personalizzata per bypassare le limitazioni di accesso",
Placeholder: "API Key Anthropic",
'Utilizza una chiave Anthropic personalizzata per bypassare le limitazioni di accesso',
Placeholder: 'API Key Anthropic',
},
Endpoint: {
Title: "Indirizzo dell'interfaccia",
SubTitle: "Esempio:",
Title: 'Indirizzo dell\'interfaccia',
SubTitle: 'Esempio:',
},
ApiVerion: {
Title: "Versione dell'interfaccia (versione api claude)",
SubTitle: "Scegli una versione API specifica",
Title: 'Versione dell\'interfaccia (versione api claude)',
SubTitle: 'Scegli una versione API specifica',
},
},
Google: {
ApiKey: {
Title: "API Key",
SubTitle: "Ottieni la tua chiave API da Google AI",
Placeholder: "Inserisci la tua chiave API Google AI Studio",
Title: 'API Key',
SubTitle: 'Ottieni la tua chiave API da Google AI',
Placeholder: 'Inserisci la tua chiave API Google AI Studio',
},
Endpoint: {
Title: "Indirizzo dell'interfaccia",
SubTitle: "Esempio:",
Title: 'Indirizzo dell\'interfaccia',
SubTitle: 'Esempio:',
},
ApiVersion: {
Title: "Versione API (solo per gemini-pro)",
SubTitle: "Scegli una versione API specifica",
Title: 'Versione API (solo per gemini-pro)',
SubTitle: 'Scegli una versione API specifica',
},
GoogleSafetySettings: {
Title: "Livello di filtraggio sicurezza Google",
SubTitle: "Imposta il livello di filtraggio dei contenuti",
Title: 'Livello di filtraggio sicurezza Google',
SubTitle: 'Imposta il livello di filtraggio dei contenuti',
},
},
Baidu: {
ApiKey: {
Title: "API Key",
SubTitle: "Utilizza una chiave API Baidu personalizzata",
Placeholder: "API Key Baidu",
Title: 'API Key',
SubTitle: 'Utilizza una chiave API Baidu personalizzata',
Placeholder: 'API Key Baidu',
},
SecretKey: {
Title: "Secret Key",
SubTitle: "Utilizza una chiave segreta Baidu personalizzata",
Placeholder: "Secret Key Baidu",
Title: 'Secret Key',
SubTitle: 'Utilizza una chiave segreta Baidu personalizzata',
Placeholder: 'Secret Key Baidu',
},
Endpoint: {
Title: "Indirizzo dell'interfaccia",
Title: 'Indirizzo dell\'interfaccia',
SubTitle:
"Non supporta configurazioni personalizzate, andare su .env",
'Non supporta configurazioni personalizzate, andare su .env',
},
},
ByteDance: {
ApiKey: {
Title: "Chiave dell'interfaccia",
SubTitle: "Utilizza una chiave API ByteDance personalizzata",
Placeholder: "API Key ByteDance",
Title: 'Chiave dell\'interfaccia',
SubTitle: 'Utilizza una chiave API ByteDance personalizzata',
Placeholder: 'API Key ByteDance',
},
Endpoint: {
Title: "Indirizzo dell'interfaccia",
SubTitle: "Esempio:",
Title: 'Indirizzo dell\'interfaccia',
SubTitle: 'Esempio:',
},
},
Alibaba: {
ApiKey: {
Title: "Chiave dell'interfaccia",
SubTitle: "Utilizza una chiave API Alibaba Cloud personalizzata",
Placeholder: "API Key Alibaba Cloud",
Title: 'Chiave dell\'interfaccia',
SubTitle: 'Utilizza una chiave API Alibaba Cloud personalizzata',
Placeholder: 'API Key Alibaba Cloud',
},
Endpoint: {
Title: "Indirizzo dell'interfaccia",
SubTitle: "Esempio:",
Title: 'Indirizzo dell\'interfaccia',
SubTitle: 'Esempio:',
},
},
CustomModel: {
Title: "Nome del modello personalizzato",
Title: 'Nome del modello personalizzato',
SubTitle:
"Aggiungi opzioni di modelli personalizzati, separati da virgole",
'Aggiungi opzioni di modelli personalizzati, separati da virgole',
},
},
Model: "Modello (model)",
Model: 'Modello (model)',
CompressModel: {
Title: "Modello di compressione",
SubTitle: "Modello utilizzato per comprimere la cronologia",
Title: 'Modello di compressione',
SubTitle: 'Modello utilizzato per comprimere la cronologia',
},
Temperature: {
Title: "Casualità (temperature)",
SubTitle: "Valore più alto, risposte più casuali",
Title: 'Casualità (temperature)',
SubTitle: 'Valore più alto, risposte più casuali',
},
TopP: {
Title: "Campionamento nucleare (top_p)",
Title: 'Campionamento nucleare (top_p)',
SubTitle:
"Simile alla casualità, ma non cambiarlo insieme alla casualità",
'Simile alla casualità, ma non cambiarlo insieme alla casualità',
},
MaxTokens: {
Title: "Limite di token per risposta (max_tokens)",
SubTitle: "Numero massimo di token per ogni interazione",
Title: 'Limite di token per risposta (max_tokens)',
SubTitle: 'Numero massimo di token per ogni interazione',
},
PresencePenalty: {
Title: "Novità del tema (presence_penalty)",
Title: 'Novità del tema (presence_penalty)',
SubTitle:
"Valore più alto, maggiore possibilità di espandere a nuovi argomenti",
'Valore più alto, maggiore possibilità di espandere a nuovi argomenti',
},
FrequencyPenalty: {
Title: "Penalità di frequenza (frequency_penalty)",
Title: 'Penalità di frequenza (frequency_penalty)',
SubTitle:
"Valore più alto, maggiore possibilità di ridurre le ripetizioni",
'Valore più alto, maggiore possibilità di ridurre le ripetizioni',
},
},
Store: {
DefaultTopic: "Nuova chat",
BotHello: "Come posso aiutarti?",
Error: "Si è verificato un errore, riprova più tardi",
DefaultTopic: 'Nuova chat',
BotHello: 'Come posso aiutarti?',
Error: 'Si è verificato un errore, riprova più tardi',
Prompt: {
History: (content: string) =>
"Questo è un riassunto della chat storica come contesto: " + content,
`Questo è un riassunto della chat storica come contesto: ${content}`,
Topic:
"Riporta il tema di questa frase in modo conciso con quattro o cinque parole, senza spiegazioni, punteggiatura, interiezioni, testo superfluo e senza grassetto. Se non c'è un tema, rispondi direttamente con 'chit-chat'",
'Riporta il tema di questa frase in modo conciso con quattro o cinque parole, senza spiegazioni, punteggiatura, interiezioni, testo superfluo e senza grassetto. Se non c\'è un tema, rispondi direttamente con \'chit-chat\'',
Summarize:
"Riassumi brevemente il contenuto della conversazione come prompt di contesto per il seguito, mantenendolo entro 200 parole",
'Riassumi brevemente il contenuto della conversazione come prompt di contesto per il seguito, mantenendolo entro 200 parole',
},
},
Copy: {
Success: "Copiato negli appunti",
Failed: "Copia fallita, concedi i permessi per gli appunti",
Success: 'Copiato negli appunti',
Failed: 'Copia fallita, concedi i permessi per gli appunti',
},
Download: {
Success: "Contenuto scaricato nella tua directory.",
Failed: "Download fallito.",
Success: 'Contenuto scaricato nella tua directory.',
Failed: 'Download fallito.',
},
Context: {
Toast: (x: any) => `Include ${x} suggerimenti predefiniti`,
Edit: "Impostazioni della conversazione attuale",
Add: "Aggiungi una conversazione",
Clear: "Contesto cancellato",
Revert: "Ripristina contesto",
Edit: 'Impostazioni della conversazione attuale',
Add: 'Aggiungi una conversazione',
Clear: 'Contesto cancellato',
Revert: 'Ripristina contesto',
},
Plugin: {
Name: "Plugin",
Name: 'Plugin',
},
FineTuned: {
Sysmessage: "Sei un assistente",
Sysmessage: 'Sei un assistente',
},
SearchChat: {
Name: "Cerca",
Name: 'Cerca',
Page: {
Title: "Cerca nei messaggi",
Search: "Inserisci parole chiave per la ricerca",
NoResult: "Nessun risultato trovato",
NoData: "Nessun dato",
Loading: "Caricamento in corso",
Title: 'Cerca nei messaggi',
Search: 'Inserisci parole chiave per la ricerca',
NoResult: 'Nessun risultato trovato',
NoData: 'Nessun dato',
Loading: 'Caricamento in corso',
SubTitle: (count: number) => `Trovati ${count} risultati`,
},
Item: {
View: "Visualizza",
View: 'Visualizza',
},
},
Mask: {
Name: "Maschera",
Name: 'Maschera',
Page: {
Title: "Maschere dei ruoli predefiniti",
Title: 'Maschere dei ruoli predefiniti',
SubTitle: (count: number) => `${count} definizioni di ruoli predefiniti`,
Search: "Cerca maschere di ruolo",
Create: "Crea nuovo",
Search: 'Cerca maschere di ruolo',
Create: 'Crea nuovo',
},
Item: {
Info: (count: number) => `Include ${count} conversazioni predefinite`,
Chat: "Conversazione",
View: "Visualizza",
Edit: "Modifica",
Delete: "Elimina",
DeleteConfirm: "Confermi eliminazione?",
Chat: 'Conversazione',
View: 'Visualizza',
Edit: 'Modifica',
Delete: 'Elimina',
DeleteConfirm: 'Confermi eliminazione?',
},
EditModal: {
Title: (readonly: boolean) =>
`Modifica maschera predefinita ${readonly ? "(sola lettura)" : ""}`,
Download: "Scarica predefinito",
Clone: "Clona predefinito",
`Modifica maschera predefinita ${readonly ? '(sola lettura)' : ''}`,
Download: 'Scarica predefinito',
Clone: 'Clona predefinito',
},
Config: {
Avatar: "Avatar del ruolo",
Name: "Nome del ruolo",
Avatar: 'Avatar del ruolo',
Name: 'Nome del ruolo',
Sync: {
Title: "Utilizza impostazioni globali",
Title: 'Utilizza impostazioni globali',
SubTitle:
"La conversazione attuale utilizzerà le impostazioni globali del modello",
'La conversazione attuale utilizzerà le impostazioni globali del modello',
Confirm:
"Le impostazioni personalizzate della conversazione attuale verranno sovrascritte automaticamente, confermi l'attivazione delle impostazioni globali?",
'Le impostazioni personalizzate della conversazione attuale verranno sovrascritte automaticamente, confermi l\'attivazione delle impostazioni globali?',
},
HideContext: {
Title: "Nascondi conversazioni predefinite",
Title: 'Nascondi conversazioni predefinite',
SubTitle:
"Le conversazioni predefinite non appariranno nella finestra della chat dopo averle nascoste",
'Le conversazioni predefinite non appariranno nella finestra della chat dopo averle nascoste',
},
Share: {
Title: "Condividi questa maschera",
SubTitle: "Genera un link diretto a questa maschera",
Action: "Copia link",
Title: 'Condividi questa maschera',
SubTitle: 'Genera un link diretto a questa maschera',
Action: 'Copia link',
},
},
},
NewChat: {
Return: "Torna",
Skip: "Inizia subito",
NotShow: "Non mostrare più",
Return: 'Torna',
Skip: 'Inizia subito',
NotShow: 'Non mostrare più',
ConfirmNoShow:
"Confermi di disabilitare? Dopo la disabilitazione, puoi riattivare in qualsiasi momento dalle impostazioni.",
Title: "Scegli una maschera",
SubTitle: "Inizia ora e interagisci con il pensiero dietro la maschera",
More: "Vedi tutto",
'Confermi di disabilitare? Dopo la disabilitazione, puoi riattivare in qualsiasi momento dalle impostazioni.',
Title: 'Scegli una maschera',
SubTitle: 'Inizia ora e interagisci con il pensiero dietro la maschera',
More: 'Vedi tutto',
},
URLCommand: {
Code: "Codice di accesso rilevato nel link, riempirlo automaticamente?",
Code: 'Codice di accesso rilevato nel link, riempirlo automaticamente?',
Settings:
"Impostazioni predefinite rilevate nel link, riempirle automaticamente?",
'Impostazioni predefinite rilevate nel link, riempirle automaticamente?',
},
UI: {
Confirm: "Conferma",
Cancel: "Annulla",
Close: "Chiudi",
Create: "Crea",
Edit: "Modifica",
Export: "Esporta",
Import: "Importa",
Sync: "Sincronizza",
Config: "Configura",
Confirm: 'Conferma',
Cancel: 'Annulla',
Close: 'Chiudi',
Create: 'Crea',
Edit: 'Modifica',
Export: 'Esporta',
Import: 'Importa',
Sync: 'Sincronizza',
Config: 'Configura',
},
Exporter: {
Description: {
Title:
"Solo i messaggi dopo la cancellazione del contesto verranno visualizzati",
'Solo i messaggi dopo la cancellazione del contesto verranno visualizzati',
},
Model: "Modello",
Messages: "Messaggi",
Topic: "Tema",
Time: "Tempo",
Model: 'Modello',
Messages: 'Messaggi',
Topic: 'Tema',
Time: 'Tempo',
},
};

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