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' name: 🐛 Bug Report
description: 'Report an bug' description: Report an bug
title: '[Bug] ' title: '[Bug] '
labels: ['bug'] labels: [bug]
body: body:
- type: dropdown - type: dropdown
attributes: attributes:
label: '📦 Deployment Method' label: 📦 Deployment Method
multiple: true multiple: true
options: options:
- 'Official installation package' - Official installation package
- 'Vercel' - Vercel
- 'Zeabur' - Zeabur
- 'Sealos' - Sealos
- 'Netlify' - Netlify
- 'Docker' - Docker
- 'Other' - Other
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: '📌 Version' label: 📌 Version
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
attributes: attributes:
label: '💻 Operating System' label: 💻 Operating System
multiple: true multiple: true
options: options:
- 'Windows' - Windows
- 'macOS' - macOS
- 'Ubuntu' - Ubuntu
- 'Other Linux' - Other Linux
- 'iOS' - iOS
- 'iPad OS' - iPad OS
- 'Android' - Android
- 'Other' - Other
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: '📌 System Version' label: 📌 System Version
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
attributes: attributes:
label: '🌐 Browser' label: 🌐 Browser
multiple: true multiple: true
options: options:
- 'Chrome' - Chrome
- 'Edge' - Edge
- 'Safari' - Safari
- 'Firefox' - Firefox
- 'Other' - Other
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: '📌 Browser Version' label: 📌 Browser Version
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: '🐛 Bug Description' label: 🐛 Bug Description
description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail. description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: '📷 Recurrence Steps' label: 📷 Recurrence Steps
description: A clear and concise description of how to recurrence. description: A clear and concise description of how to recurrence.
- type: textarea - type: textarea
attributes: attributes:
label: '🚦 Expected Behavior' label: 🚦 Expected Behavior
description: A clear and concise description of what you expected to happen. description: A clear and concise description of what you expected to happen.
- type: textarea - type: textarea
attributes: attributes:
label: '📝 Additional Information' 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. 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: '🐛 反馈缺陷' name: 🐛 反馈缺陷
description: '反馈一个问题/缺陷' description: 反馈一个问题/缺陷
title: '[Bug] ' title: '[Bug] '
labels: ['bug'] labels: [bug]
body: body:
- type: dropdown - type: dropdown
attributes: attributes:
label: '📦 部署方式' label: 📦 部署方式
multiple: true multiple: true
options: options:
- '官方安装包' - 官方安装包
- 'Vercel' - Vercel
- 'Zeabur' - Zeabur
- 'Sealos' - Sealos
- 'Netlify' - Netlify
- 'Docker' - Docker
- 'Other' - Other
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: '📌 软件版本' label: 📌 软件版本
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
attributes: attributes:
label: '💻 系统环境' label: 💻 系统环境
multiple: true multiple: true
options: options:
- 'Windows' - Windows
- 'macOS' - macOS
- 'Ubuntu' - Ubuntu
- 'Other Linux' - Other Linux
- 'iOS' - iOS
- 'iPad OS' - iPad OS
- 'Android' - Android
- 'Other' - Other
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: '📌 系统版本' label: 📌 系统版本
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
attributes: attributes:
label: '🌐 浏览器' label: 🌐 浏览器
multiple: true multiple: true
options: options:
- 'Chrome' - Chrome
- 'Edge' - Edge
- 'Safari' - Safari
- 'Firefox' - Firefox
- 'Other' - Other
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: '📌 浏览器版本' label: 📌 浏览器版本
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: '🐛 问题描述' label: 🐛 问题描述
description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。 description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: '📷 复现步骤' label: 📷 复现步骤
description: 请提供一个清晰且简洁的描述,说明如何复现问题。 description: 请提供一个清晰且简洁的描述,说明如何复现问题。
- type: textarea - type: textarea
attributes: attributes:
label: '🚦 期望结果' label: 🚦 期望结果
description: 请提供一个清晰且简洁的描述,说明您期望发生什么。 description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
- type: textarea - type: textarea
attributes: attributes:
label: '📝 补充信息' label: 📝 补充信息
description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。 description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,52 +1,45 @@
name: Publish Docker image name: Publish Docker image
on: on:
workflow_dispatch: workflow_dispatch:
release: release:
types: [published] types: [published]
jobs: jobs:
push_to_registry: push_to_registry:
name: Push Docker image to Docker Hub name: Push Docker image to Docker Hub
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Check out the repo
name: Check out the repo uses: actions/checkout@v3
uses: actions/checkout@v3 - name: Log in to Docker Hub
- uses: docker/login-action@v2
name: Log in to Docker Hub with:
uses: docker/login-action@v2 username: ${{ secrets.DOCKER_USERNAME }}
with: password: ${{ secrets.DOCKER_PASSWORD }}
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
- - name: Extract metadata (tags, labels) for Docker
name: Set up Docker Buildx id: meta
uses: docker/setup-buildx-action@v2 uses: docker/metadata-action@v4
with:
- images: yidadaa/chatgpt-next-web
name: Build and push Docker image tags: |
uses: docker/build-push-action@v4 type=raw,value=latest
with: type=ref,event=tag
context: .
platforms: linux/amd64,linux/arm64 - name: Set up QEMU
push: true uses: docker/setup-qemu-action@v2
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} - name: Set up Docker Buildx
cache-from: type=gha uses: docker/setup-buildx-action@v2
cache-to: type=gha,mode=max
- 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 name: Issue Translator
on: on:
issue_comment: issue_comment:
types: [created] types: [created]
issues: issues:
types: [opened] types: [opened]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: usthe/issues-translate-action@v2.7 - uses: usthe/issues-translate-action@v2.7
with: with:
IS_MODIFY_TITLE: false IS_MODIFY_TITLE: false
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 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 name: Upstream Sync
permissions: permissions:
contents: write contents: write
on: on:
schedule: schedule:
- cron: "0 0 * * *" # every day - cron: '0 0 * * *' # every day
workflow_dispatch: workflow_dispatch:
jobs: jobs:
sync_latest_from_upstream: sync_latest_from_upstream:
name: Sync latest commits from upstream repo name: Sync latest commits from upstream repo
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.repository.fork }} if: ${{ github.event.repository.fork }}
steps: steps:
# Step 1: run a standard checkout action # Step 1: run a standard checkout action
- name: Checkout target repo - name: Checkout target repo
uses: actions/checkout@v3 uses: actions/checkout@v3
# Step 2: run the sync action # Step 2: run the sync action
- name: Sync upstream changes - name: Sync upstream changes
id: sync id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with: with:
upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web
upstream_sync_branch: main upstream_sync_branch: main
target_sync_branch: sync-main target_sync_branch: sync-main
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
# Set test_mode true to run tests instead of the true action!! # Set test_mode true to run tests instead of the true action!!
test_mode: false test_mode: false
- name: Sync check - name: Sync check
if: failure() if: failure()
run: | 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] 由于上游仓库的 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" 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 exit 1

View File

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

View File

@@ -1,18 +1,18 @@
import { ApiPath } from "@/app/constant"; import type { NextRequest } from 'next/server';
import { NextRequest } from "next/server"; import { ApiPath } from '@/app/constant';
import { handle as openaiHandler } from "../../openai"; import { handle as alibabaHandler } from '../../alibaba';
import { handle as azureHandler } from "../../azure"; import { handle as anthropicHandler } from '../../anthropic';
import { handle as googleHandler } from "../../google"; import { handle as azureHandler } from '../../azure';
import { handle as anthropicHandler } from "../../anthropic"; import { handle as baiduHandler } from '../../baidu';
import { handle as baiduHandler } from "../../baidu"; import { handle as bytedanceHandler } from '../../bytedance';
import { handle as bytedanceHandler } from "../../bytedance"; import { handle as chatglmHandler } from '../../glm';
import { handle as alibabaHandler } from "../../alibaba"; import { handle as googleHandler } from '../../google';
import { handle as moonshotHandler } from "../../moonshot"; import { handle as iflytekHandler } from '../../iflytek';
import { handle as stabilityHandler } from "../../stability"; import { handle as moonshotHandler } from '../../moonshot';
import { handle as iflytekHandler } from "../../iflytek"; import { handle as openaiHandler } from '../../openai';
import { handle as xaiHandler } from "../../xai"; import { handle as proxyHandler } from '../../proxy';
import { handle as chatglmHandler } from "../../glm"; import { handle as stabilityHandler } from '../../stability';
import { handle as proxyHandler } from "../../proxy"; import { handle as xaiHandler } from '../../xai';
async function handle( async function handle(
req: NextRequest, req: NextRequest,
@@ -54,23 +54,23 @@ async function handle(
export const GET = handle; export const GET = handle;
export const POST = handle; export const POST = handle;
export const runtime = "edge"; export const runtime = 'edge';
export const preferredRegion = [ export const preferredRegion = [
"arn1", 'arn1',
"bom1", 'bom1',
"cdg1", 'cdg1',
"cle1", 'cle1',
"cpt1", 'cpt1',
"dub1", 'dub1',
"fra1", 'fra1',
"gru1", 'gru1',
"hnd1", 'hnd1',
"iad1", 'iad1',
"icn1", 'icn1',
"kix1", 'kix1',
"lhr1", 'lhr1',
"pdx1", 'pdx1',
"sfo1", 'sfo1',
"sin1", 'sin1',
"syd1", '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 { import {
ALIBABA_BASE_URL, ALIBABA_BASE_URL,
ApiPath, ApiPath,
ModelProvider, ModelProvider,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; } from '@/app/constant';
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from '@/app/utils/format';
import { NextRequest, NextResponse } from "next/server"; import { isModelAvailableInServer } from '@/app/utils/model';
import { auth } from "@/app/api/auth"; import { NextResponse } from 'next/server';
import { isModelAvailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
@@ -16,10 +17,10 @@ export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
console.log("[Alibaba Route] params ", params); console.log('[Alibaba Route] params ', params);
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const authResult = auth(req, ModelProvider.Qwen); const authResult = auth(req, ModelProvider.Qwen);
@@ -33,7 +34,7 @@ export async function handle(
const response = await request(req); const response = await request(req);
return response; return response;
} catch (e) { } catch (e) {
console.error("[Alibaba] ", e); console.error('[Alibaba] ', e);
return NextResponse.json(prettyObject(e)); return NextResponse.json(prettyObject(e));
} }
} }
@@ -42,20 +43,20 @@ async function request(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
// alibaba use base url or just remove the path // 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; let baseUrl = serverConfig.alibabaUrl || ALIBABA_BASE_URL;
if (!baseUrl.startsWith("http")) { if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`; baseUrl = `https://${baseUrl}`;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Proxy] ", path); console.log('[Proxy] ', path);
console.log("[Base Url]", baseUrl); console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => { () => {
@@ -67,15 +68,15 @@ async function request(req: NextRequest) {
const fetchUrl = `${baseUrl}${path}`; const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
Authorization: req.headers.get("Authorization") ?? "", 'Authorization': req.headers.get('Authorization') ?? '',
"X-DashScope-SSE": req.headers.get("X-DashScope-SSE") ?? "disable", 'X-DashScope-SSE': req.headers.get('X-DashScope-SSE') ?? 'disable',
}, },
method: req.method, method: req.method,
body: req.body, body: req.body,
redirect: "manual", redirect: 'manual',
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
signal: controller.signal, signal: controller.signal,
}; };
@@ -114,9 +115,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete('www-authenticate');
// to disable nginx buffering // to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no"); newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, { return new Response(res.body, {
status: res.status, 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 { import {
ANTHROPIC_BASE_URL,
Anthropic, Anthropic,
ANTHROPIC_BASE_URL,
ApiPath, ApiPath,
ServiceProvider,
ModelProvider, ModelProvider,
} from "@/app/constant"; ServiceProvider,
import { prettyObject } from "@/app/utils/format"; } from '@/app/constant';
import { NextRequest, NextResponse } from "next/server"; import { cloudflareAIGatewayUrl } from '@/app/utils/cloudflare';
import { auth } from "./auth"; import { prettyObject } from '@/app/utils/format';
import { isModelAvailableInServer } from "@/app/utils/model"; import { isModelAvailableInServer } from '@/app/utils/model';
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { NextResponse } from 'next/server';
import { auth } from './auth';
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]); const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
@@ -18,20 +19,20 @@ export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
console.log("[Anthropic Route] params ", params); console.log('[Anthropic Route] params ', params);
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const subpath = params.path.join("/"); const subpath = params.path.join('/');
if (!ALLOWD_PATH.has(subpath)) { if (!ALLOWD_PATH.has(subpath)) {
console.log("[Anthropic Route] forbidden path ", subpath); console.log('[Anthropic Route] forbidden path ', subpath);
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
msg: "you are not allowed to request " + subpath, msg: `you are not allowed to request ${subpath}`,
}, },
{ {
status: 403, status: 403,
@@ -50,7 +51,7 @@ export async function handle(
const response = await request(req); const response = await request(req);
return response; return response;
} catch (e) { } catch (e) {
console.error("[Anthropic] ", e); console.error('[Anthropic] ', e);
return NextResponse.json(prettyObject(e)); return NextResponse.json(prettyObject(e));
} }
} }
@@ -60,28 +61,28 @@ const serverConfig = getServerSideConfig();
async function request(req: NextRequest) { async function request(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
let authHeaderName = "x-api-key"; const authHeaderName = 'x-api-key';
let authValue = const authValue
req.headers.get(authHeaderName) || = req.headers.get(authHeaderName)
req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() || || req.headers.get('Authorization')?.replaceAll('Bearer ', '').trim()
serverConfig.anthropicApiKey || || serverConfig.anthropicApiKey
""; || '';
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, ""); const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, '');
let baseUrl = let baseUrl
serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL; = serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
if (!baseUrl.startsWith("http")) { if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`; baseUrl = `https://${baseUrl}`;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Proxy] ", path); console.log('[Proxy] ', path);
console.log("[Base Url]", baseUrl); console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => { () => {
@@ -95,20 +96,20 @@ async function request(req: NextRequest) {
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
"Cache-Control": "no-store", 'Cache-Control': 'no-store',
"anthropic-dangerous-direct-browser-access": "true", 'anthropic-dangerous-direct-browser-access': 'true',
[authHeaderName]: authValue, [authHeaderName]: authValue,
"anthropic-version": 'anthropic-version':
req.headers.get("anthropic-version") || req.headers.get('anthropic-version')
serverConfig.anthropicApiVersion || || serverConfig.anthropicApiVersion
Anthropic.Vision, || Anthropic.Vision,
}, },
method: req.method, method: req.method,
body: req.body, body: req.body,
redirect: "manual", redirect: 'manual',
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
signal: controller.signal, signal: controller.signal,
}; };
@@ -155,9 +156,9 @@ async function request(req: NextRequest) {
// ); // );
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete('www-authenticate');
// to disable nginx buffering // to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no"); newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, { return new Response(res.body, {
status: res.status, status: res.status,

View File

@@ -1,6 +1,7 @@
import md5 from "spark-md5"; import type { NextRequest } from 'next/server';
import { NextRequest, NextResponse } from "next/server"; import { getServerSideConfig } from '@/app/config/server';
import { getServerSideConfig } from "@/app/config/server"; import { NextResponse } from 'next/server';
import md5 from 'spark-md5';
async function handle(req: NextRequest, res: NextResponse) { async function handle(req: NextRequest, res: NextResponse) {
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
@@ -9,7 +10,7 @@ async function handle(req: NextRequest, res: NextResponse) {
const storeHeaders = () => ({ const storeHeaders = () => ({
Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`, Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
}); });
if (req.method === "POST") { if (req.method === 'POST') {
const clonedBody = await req.text(); const clonedBody = await req.text();
const hashedCode = md5.hash(clonedBody).trim(); const hashedCode = md5.hash(clonedBody).trim();
const body: { const body: {
@@ -21,9 +22,9 @@ async function handle(req: NextRequest, res: NextResponse) {
value: clonedBody, value: clonedBody,
}; };
try { try {
const ttl = parseInt(serverConfig.cloudflareKVTTL as string); const ttl = Number.parseInt(serverConfig.cloudflareKVTTL as string);
if (ttl > 60) { if (ttl > 60) {
body["expiration_ttl"] = ttl; body.expiration_ttl = ttl;
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -31,13 +32,13 @@ async function handle(req: NextRequest, res: NextResponse) {
const res = await fetch(`${storeUrl()}/bulk`, { const res = await fetch(`${storeUrl()}/bulk`, {
headers: { headers: {
...storeHeaders(), ...storeHeaders(),
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
method: "PUT", method: 'PUT',
body: JSON.stringify([body]), body: JSON.stringify([body]),
}); });
const result = await res.json(); const result = await res.json();
console.log("save data", result); console.log('save data', result);
if (result?.success) { if (result?.success) {
return NextResponse.json( return NextResponse.json(
{ code: 0, id: hashedCode, result }, { code: 0, id: hashedCode, result },
@@ -45,15 +46,15 @@ async function handle(req: NextRequest, res: NextResponse) {
); );
} }
return NextResponse.json( return NextResponse.json(
{ error: true, msg: "Save data error" }, { error: true, msg: 'Save data error' },
{ status: 400 }, { status: 400 },
); );
} }
if (req.method === "GET") { if (req.method === 'GET') {
const id = req?.nextUrl?.searchParams?.get("id"); const id = req?.nextUrl?.searchParams?.get('id');
const res = await fetch(`${storeUrl()}/values/${id}`, { const res = await fetch(`${storeUrl()}/values/${id}`, {
headers: storeHeaders(), headers: storeHeaders(),
method: "GET", method: 'GET',
}); });
return new Response(res.body, { return new Response(res.body, {
status: res.status, status: res.status,
@@ -62,7 +63,7 @@ async function handle(req: NextRequest, res: NextResponse) {
}); });
} }
return NextResponse.json( return NextResponse.json(
{ error: true, msg: "Invalid request" }, { error: true, msg: 'Invalid request' },
{ status: 400 }, { status: 400 },
); );
} }
@@ -70,4 +71,4 @@ async function handle(req: NextRequest, res: NextResponse) {
export const POST = handle; export const POST = handle;
export const GET = 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 type { NextRequest } from 'next/server';
import { getServerSideConfig } from "../config/server"; import md5 from 'spark-md5';
import md5 from "spark-md5"; import { getServerSideConfig } from '../config/server';
import { ACCESS_CODE_PREFIX, ModelProvider } from "../constant"; import { ACCESS_CODE_PREFIX, ModelProvider } from '../constant';
function getIP(req: NextRequest) { function getIP(req: NextRequest) {
let ip = req.ip ?? req.headers.get("x-real-ip"); let ip = req.ip ?? req.headers.get('x-real-ip');
const forwardedFor = req.headers.get("x-forwarded-for"); const forwardedFor = req.headers.get('x-forwarded-for');
if (!ip && forwardedFor) { if (!ip && forwardedFor) {
ip = forwardedFor.split(",").at(0) ?? ""; ip = forwardedFor.split(',').at(0) ?? '';
} }
return ip; return ip;
} }
function parseApiKey(bearToken: string) { 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); const isApiKey = !token.startsWith(ACCESS_CODE_PREFIX);
return { return {
accessCode: isApiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length), accessCode: isApiKey ? '' : token.slice(ACCESS_CODE_PREFIX.length),
apiKey: isApiKey ? token : "", apiKey: isApiKey ? token : '',
}; };
} }
export function auth(req: NextRequest, modelProvider: ModelProvider) { 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 // check if it is openai api key or user token
const { accessCode, apiKey } = parseApiKey(authToken); const { accessCode, apiKey } = parseApiKey(authToken);
const hashedCode = md5.hash(accessCode ?? "").trim(); const hashedCode = md5.hash(accessCode ?? '').trim();
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]); console.log('[Auth] allowed hashed codes: ', [...serverConfig.codes]);
console.log("[Auth] got access code:", accessCode); console.log('[Auth] got access code:', accessCode);
console.log("[Auth] hashed access code:", hashedCode); console.log('[Auth] hashed access code:', hashedCode);
console.log("[User IP] ", getIP(req)); console.log('[User IP] ', getIP(req));
console.log("[Time] ", new Date().toLocaleString()); console.log('[Time] ', new Date().toLocaleString());
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) { if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
return { return {
error: true, error: true,
msg: !accessCode ? "empty access code" : "wrong access code", msg: !accessCode ? 'empty access code' : 'wrong access code',
}; };
} }
if (serverConfig.hideUserApiKey && !!apiKey) { if (serverConfig.hideUserApiKey && !!apiKey) {
return { return {
error: true, 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; systemApiKey = serverConfig.moonshotApiKey;
break; break;
case ModelProvider.Iflytek: case ModelProvider.Iflytek:
systemApiKey = systemApiKey
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret; = `${serverConfig.iflytekApiKey}:${serverConfig.iflytekApiSecret}`;
break; break;
case ModelProvider.XAI: case ModelProvider.XAI:
systemApiKey = serverConfig.xaiApiKey; systemApiKey = serverConfig.xaiApiKey;
@@ -100,7 +100,7 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
break; break;
case ModelProvider.GPT: case ModelProvider.GPT:
default: default:
if (req.nextUrl.pathname.includes("azure/deployments")) { if (req.nextUrl.pathname.includes('azure/deployments')) {
systemApiKey = serverConfig.azureApiKey; systemApiKey = serverConfig.azureApiKey;
} else { } else {
systemApiKey = serverConfig.apiKey; systemApiKey = serverConfig.apiKey;
@@ -108,13 +108,13 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
} }
if (systemApiKey) { if (systemApiKey) {
console.log("[Auth] use system api key"); console.log('[Auth] use system api key');
req.headers.set("Authorization", `Bearer ${systemApiKey}`); req.headers.set('Authorization', `Bearer ${systemApiKey}`);
} else { } else {
console.log("[Auth] admin did not provide an api key"); console.log('[Auth] admin did not provide an api key');
} }
} else { } else {
console.log("[Auth] use user api key"); console.log('[Auth] use user api key');
} }
return { return {

View File

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

View File

@@ -1,47 +1,48 @@
import { NextRequest, NextResponse } from "next/server"; import type { NextRequest } from 'next/server';
import { getServerSideConfig } from "../config/server"; import { NextResponse } from 'next/server';
import { OPENAI_BASE_URL, ServiceProvider } from "../constant"; import { getServerSideConfig } from '../config/server';
import { cloudflareAIGatewayUrl } from "../utils/cloudflare"; import { OPENAI_BASE_URL, ServiceProvider } from '../constant';
import { getModelProvider, isModelAvailableInServer } from "../utils/model"; import { cloudflareAIGatewayUrl } from '../utils/cloudflare';
import { getModelProvider, isModelAvailableInServer } from '../utils/model';
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
export async function requestOpenai(req: NextRequest) { export async function requestOpenai(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
const isAzure = req.nextUrl.pathname.includes("azure/deployments"); const isAzure = req.nextUrl.pathname.includes('azure/deployments');
var authValue, let authValue;
authHeaderName = ""; let authHeaderName = '';
if (isAzure) { if (isAzure) {
authValue = authValue
req.headers = req.headers
.get("Authorization") .get('Authorization')
?.trim() ?.trim()
.replaceAll("Bearer ", "") .replaceAll('Bearer ', '')
.trim() ?? ""; .trim() ?? '';
authHeaderName = "api-key"; authHeaderName = 'api-key';
} else { } else {
authValue = req.headers.get("Authorization") ?? ""; authValue = req.headers.get('Authorization') ?? '';
authHeaderName = "Authorization"; authHeaderName = 'Authorization';
} }
let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", ""); let path = `${req.nextUrl.pathname}`.replaceAll('/api/openai/', '');
let baseUrl = let baseUrl
(isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL; = (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
if (!baseUrl.startsWith("http")) { if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`; baseUrl = `https://${baseUrl}`;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Proxy] ", path); console.log('[Proxy] ', path);
console.log("[Base Url]", baseUrl); console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => { () => {
@@ -51,30 +52,30 @@ export async function requestOpenai(req: NextRequest) {
); );
if (isAzure) { if (isAzure) {
const azureApiVersion = const azureApiVersion
req?.nextUrl?.searchParams?.get("api-version") || = req?.nextUrl?.searchParams?.get('api-version')
serverConfig.azureApiVersion; || serverConfig.azureApiVersion;
baseUrl = baseUrl.split("/deployments").shift() as string; baseUrl = baseUrl.split('/deployments').shift() as string;
path = `${req.nextUrl.pathname.replaceAll( path = `${req.nextUrl.pathname.replaceAll(
"/api/azure/", '/api/azure/',
"", '',
)}?api-version=${azureApiVersion}`; )}?api-version=${azureApiVersion}`;
// Forward compatibility: // Forward compatibility:
// if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL // if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL
// then using default '{deploy-id}' // then using default '{deploy-id}'
if (serverConfig.customModels && serverConfig.azureUrl) { if (serverConfig.customModels && serverConfig.azureUrl) {
const modelName = path.split("/")[1]; const modelName = path.split('/')[1];
let realDeployName = ""; let realDeployName = '';
serverConfig.customModels serverConfig.customModels
.split(",") .split(',')
.filter((v) => !!v && !v.startsWith("-") && v.includes(modelName)) .filter(v => !!v && !v.startsWith('-') && v.includes(modelName))
.forEach((m) => { .forEach((m) => {
const [fullName, displayName] = m.split("="); const [fullName, displayName] = m.split('=');
const [_, providerName] = getModelProvider(fullName); const [_, providerName] = getModelProvider(fullName);
if (providerName === "azure" && !displayName) { if (providerName === 'azure' && !displayName) {
const [_, deployId] = (serverConfig?.azureUrl ?? "").split( const [_, deployId] = (serverConfig?.azureUrl ?? '').split(
"deployments/", 'deployments/',
); );
if (deployId) { if (deployId) {
realDeployName = deployId; realDeployName = deployId;
@@ -82,29 +83,29 @@ export async function requestOpenai(req: NextRequest) {
} }
}); });
if (realDeployName) { if (realDeployName) {
console.log("[Replace with DeployId", realDeployName); console.log('[Replace with DeployId', realDeployName);
path = path.replaceAll(modelName, realDeployName); path = path.replaceAll(modelName, realDeployName);
} }
} }
} }
const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`); const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
console.log("fetchUrl", fetchUrl); console.log('fetchUrl', fetchUrl);
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
"Cache-Control": "no-store", 'Cache-Control': 'no-store',
[authHeaderName]: authValue, [authHeaderName]: authValue,
...(serverConfig.openaiOrgId && { ...(serverConfig.openaiOrgId && {
"OpenAI-Organization": serverConfig.openaiOrgId, 'OpenAI-Organization': serverConfig.openaiOrgId,
}), }),
}, },
method: req.method, method: req.method,
body: req.body, body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual", redirect: 'manual',
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
signal: controller.signal, signal: controller.signal,
}; };
@@ -122,8 +123,8 @@ export async function requestOpenai(req: NextRequest) {
serverConfig.customModels, serverConfig.customModels,
jsonBody?.model as string, jsonBody?.model as string,
ServiceProvider.OpenAI as string, ServiceProvider.OpenAI as string,
) || )
isModelAvailableInServer( || isModelAvailableInServer(
serverConfig.customModels, serverConfig.customModels,
jsonBody?.model as string, jsonBody?.model as string,
ServiceProvider.Azure as string, ServiceProvider.Azure as string,
@@ -140,7 +141,7 @@ export async function requestOpenai(req: NextRequest) {
); );
} }
} catch (e) { } 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); const res = await fetch(fetchUrl, fetchOptions);
// Extract the OpenAI-Organization header from the response // 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 // 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 // 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 { } else {
console.log("[Org ID] is not set up."); console.log('[Org ID] is not set up.');
} }
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete('www-authenticate');
// to disable nginx buffering // 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) // 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 // Also, this is to prevent the header from being sent to the client
if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") { if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === '') {
newHeaders.delete("OpenAI-Organization"); newHeaders.delete('OpenAI-Organization');
} }
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response // 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 // 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 // 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 // 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, { return new Response(res.body, {
status: res.status, 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(); const serverConfig = getServerSideConfig();
@@ -27,4 +27,4 @@ async function handle() {
export const GET = handle; export const GET = handle;
export const POST = 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 { import {
CHATGLM_BASE_URL,
ApiPath, ApiPath,
CHATGLM_BASE_URL,
ModelProvider, ModelProvider,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; } from '@/app/constant';
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from '@/app/utils/format';
import { NextRequest, NextResponse } from "next/server"; import { isModelAvailableInServer } from '@/app/utils/model';
import { auth } from "@/app/api/auth"; import { NextResponse } from 'next/server';
import { isModelAvailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
@@ -16,10 +17,10 @@ export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
console.log("[GLM Route] params ", params); console.log('[GLM Route] params ', params);
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const authResult = auth(req, ModelProvider.ChatGLM); const authResult = auth(req, ModelProvider.ChatGLM);
@@ -33,7 +34,7 @@ export async function handle(
const response = await request(req); const response = await request(req);
return response; return response;
} catch (e) { } catch (e) {
console.error("[GLM] ", e); console.error('[GLM] ', e);
return NextResponse.json(prettyObject(e)); return NextResponse.json(prettyObject(e));
} }
} }
@@ -42,20 +43,20 @@ async function request(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
// alibaba use base url or just remove the path // 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; let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL;
if (!baseUrl.startsWith("http")) { if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`; baseUrl = `https://${baseUrl}`;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Proxy] ", path); console.log('[Proxy] ', path);
console.log("[Base Url]", baseUrl); console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => { () => {
@@ -65,17 +66,17 @@ async function request(req: NextRequest) {
); );
const fetchUrl = `${baseUrl}${path}`; const fetchUrl = `${baseUrl}${path}`;
console.log("[Fetch Url] ", fetchUrl); console.log('[Fetch Url] ', fetchUrl);
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
Authorization: req.headers.get("Authorization") ?? "", 'Authorization': req.headers.get('Authorization') ?? '',
}, },
method: req.method, method: req.method,
body: req.body, body: req.body,
redirect: "manual", redirect: 'manual',
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
signal: controller.signal, signal: controller.signal,
}; };
@@ -114,9 +115,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete('www-authenticate');
// to disable nginx buffering // to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no"); newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, { return new Response(res.body, {
status: res.status, status: res.status,

View File

@@ -1,8 +1,9 @@
import { NextRequest, NextResponse } from "next/server"; import type { NextRequest } from 'next/server';
import { auth } from "./auth"; import { getServerSideConfig } from '@/app/config/server';
import { getServerSideConfig } from "@/app/config/server"; import { ApiPath, GEMINI_BASE_URL, ModelProvider } from '@/app/constant';
import { ApiPath, GEMINI_BASE_URL, ModelProvider } from "@/app/constant"; import { prettyObject } from '@/app/utils/format';
import { prettyObject } from "@/app/utils/format"; import { NextResponse } from 'next/server';
import { auth } from './auth';
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
@@ -10,10 +11,10 @@ export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { provider: string; path: string[] } }, { params }: { params: { provider: string; path: string[] } },
) { ) {
console.log("[Google Route] params ", params); console.log('[Google Route] params ', params);
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const authResult = auth(req, ModelProvider.GeminiPro); const authResult = auth(req, ModelProvider.GeminiPro);
@@ -23,11 +24,11 @@ export async function handle(
}); });
} }
const bearToken = const bearToken
req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || ""; = req.headers.get('x-goog-api-key') || req.headers.get('Authorization') || '';
const token = bearToken.trim().replaceAll("Bearer ", "").trim(); const token = bearToken.trim().replaceAll('Bearer ', '').trim();
const apiKey = token ? token : serverConfig.googleApiKey; const apiKey = token || serverConfig.googleApiKey;
if (!apiKey) { if (!apiKey) {
return NextResponse.json( return NextResponse.json(
@@ -44,7 +45,7 @@ export async function handle(
const response = await request(req, apiKey); const response = await request(req, apiKey);
return response; return response;
} catch (e) { } catch (e) {
console.error("[Google] ", e); console.error('[Google] ', e);
return NextResponse.json(prettyObject(e)); return NextResponse.json(prettyObject(e));
} }
} }
@@ -52,20 +53,20 @@ export async function handle(
export const GET = handle; export const GET = handle;
export const POST = handle; export const POST = handle;
export const runtime = "edge"; export const runtime = 'edge';
export const preferredRegion = [ export const preferredRegion = [
"bom1", 'bom1',
"cle1", 'cle1',
"cpt1", 'cpt1',
"gru1", 'gru1',
"hnd1", 'hnd1',
"iad1", 'iad1',
"icn1", 'icn1',
"kix1", 'kix1',
"pdx1", 'pdx1',
"sfo1", 'sfo1',
"sin1", 'sin1',
"syd1", 'syd1',
]; ];
async function request(req: NextRequest, apiKey: string) { 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 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}`; baseUrl = `https://${baseUrl}`;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Proxy] ", path); console.log('[Proxy] ', path);
console.log("[Base Url]", baseUrl); console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => { () => {
@@ -93,24 +94,24 @@ async function request(req: NextRequest, apiKey: string) {
10 * 60 * 1000, 10 * 60 * 1000,
); );
const fetchUrl = `${baseUrl}${path}${ 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 = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
"Cache-Control": "no-store", 'Cache-Control': 'no-store',
"x-goog-api-key": 'x-goog-api-key':
req.headers.get("x-goog-api-key") || req.headers.get('x-goog-api-key')
(req.headers.get("Authorization") ?? "").replace("Bearer ", ""), || (req.headers.get('Authorization') ?? '').replace('Bearer ', ''),
}, },
method: req.method, method: req.method,
body: req.body, body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual", redirect: 'manual',
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
signal: controller.signal, signal: controller.signal,
}; };
@@ -118,9 +119,9 @@ async function request(req: NextRequest, apiKey: string) {
const res = await fetch(fetchUrl, fetchOptions); const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete('www-authenticate');
// to disable nginx buffering // to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no"); newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, { return new Response(res.body, {
status: res.status, 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 { import {
IFLYTEK_BASE_URL,
ApiPath, ApiPath,
IFLYTEK_BASE_URL,
ModelProvider, ModelProvider,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; } from '@/app/constant';
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from '@/app/utils/format';
import { NextRequest, NextResponse } from "next/server"; import { isModelAvailableInServer } from '@/app/utils/model';
import { auth } from "@/app/api/auth"; import { NextResponse } from 'next/server';
import { isModelAvailableInServer } from "@/app/utils/model";
// iflytek // iflytek
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
@@ -17,10 +18,10 @@ export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
console.log("[Iflytek Route] params ", params); console.log('[Iflytek Route] params ', params);
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const authResult = auth(req, ModelProvider.Iflytek); const authResult = auth(req, ModelProvider.Iflytek);
@@ -34,7 +35,7 @@ export async function handle(
const response = await request(req); const response = await request(req);
return response; return response;
} catch (e) { } catch (e) {
console.error("[Iflytek] ", e); console.error('[Iflytek] ', e);
return NextResponse.json(prettyObject(e)); return NextResponse.json(prettyObject(e));
} }
} }
@@ -43,20 +44,20 @@ async function request(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
// iflytek use base url or just remove the path // 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; let baseUrl = serverConfig.iflytekUrl || IFLYTEK_BASE_URL;
if (!baseUrl.startsWith("http")) { if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`; baseUrl = `https://${baseUrl}`;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Proxy] ", path); console.log('[Proxy] ', path);
console.log("[Base Url]", baseUrl); console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => { () => {
@@ -68,14 +69,14 @@ async function request(req: NextRequest) {
const fetchUrl = `${baseUrl}${path}`; const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
Authorization: req.headers.get("Authorization") ?? "", 'Authorization': req.headers.get('Authorization') ?? '',
}, },
method: req.method, method: req.method,
body: req.body, body: req.body,
redirect: "manual", redirect: 'manual',
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
signal: controller.signal, signal: controller.signal,
}; };
@@ -114,9 +115,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete('www-authenticate');
// to disable nginx buffering // to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no"); newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, { return new Response(res.body, {
status: res.status, 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 { import {
MOONSHOT_BASE_URL,
ApiPath, ApiPath,
ModelProvider, ModelProvider,
MOONSHOT_BASE_URL,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; } from '@/app/constant';
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from '@/app/utils/format';
import { NextRequest, NextResponse } from "next/server"; import { isModelAvailableInServer } from '@/app/utils/model';
import { auth } from "@/app/api/auth"; import { NextResponse } from 'next/server';
import { isModelAvailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
@@ -16,10 +17,10 @@ export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
console.log("[Moonshot Route] params ", params); console.log('[Moonshot Route] params ', params);
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const authResult = auth(req, ModelProvider.Moonshot); const authResult = auth(req, ModelProvider.Moonshot);
@@ -33,7 +34,7 @@ export async function handle(
const response = await request(req); const response = await request(req);
return response; return response;
} catch (e) { } catch (e) {
console.error("[Moonshot] ", e); console.error('[Moonshot] ', e);
return NextResponse.json(prettyObject(e)); return NextResponse.json(prettyObject(e));
} }
} }
@@ -42,20 +43,20 @@ async function request(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
// alibaba use base url or just remove the path // 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; let baseUrl = serverConfig.moonshotUrl || MOONSHOT_BASE_URL;
if (!baseUrl.startsWith("http")) { if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`; baseUrl = `https://${baseUrl}`;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Proxy] ", path); console.log('[Proxy] ', path);
console.log("[Base Url]", baseUrl); console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => { () => {
@@ -67,14 +68,14 @@ async function request(req: NextRequest) {
const fetchUrl = `${baseUrl}${path}`; const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
Authorization: req.headers.get("Authorization") ?? "", 'Authorization': req.headers.get('Authorization') ?? '',
}, },
method: req.method, method: req.method,
body: req.body, body: req.body,
redirect: "manual", redirect: 'manual',
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
signal: controller.signal, signal: controller.signal,
}; };
@@ -113,9 +114,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete('www-authenticate');
// to disable nginx buffering // to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no"); newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, { return new Response(res.body, {
status: res.status, status: res.status,

View File

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

View File

@@ -1,32 +1,33 @@
import { NextRequest, NextResponse } from "next/server"; import type { NextRequest } from 'next/server';
import { getServerSideConfig } from "@/app/config/server"; import { getServerSideConfig } from '@/app/config/server';
import { NextResponse } from 'next/server';
export async function handle( export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
console.log("[Proxy Route] params ", params); console.log('[Proxy Route] params ', params);
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
// remove path params from searchParams // remove path params from searchParams
req.nextUrl.searchParams.delete("path"); req.nextUrl.searchParams.delete('path');
req.nextUrl.searchParams.delete("provider"); req.nextUrl.searchParams.delete('provider');
const subpath = params.path.join("/"); const subpath = params.path.join('/');
const fetchUrl = `${req.headers.get( const fetchUrl = `${req.headers.get(
"x-base-url", 'x-base-url',
)}/${subpath}?${req.nextUrl.searchParams.toString()}`; )}/${subpath}?${req.nextUrl.searchParams.toString()}`;
const skipHeaders = ["connection", "host", "origin", "referer", "cookie"]; const skipHeaders = ['connection', 'host', 'origin', 'referer', 'cookie'];
const headers = new Headers( const headers = new Headers(
Array.from(req.headers.entries()).filter((item) => { Array.from(req.headers.entries()).filter((item) => {
if ( if (
item[0].indexOf("x-") > -1 || item[0].includes('x-')
item[0].indexOf("sec-") > -1 || || item[0].includes('sec-')
skipHeaders.includes(item[0]) || skipHeaders.includes(item[0])
) { ) {
return false; return false;
} }
@@ -34,16 +35,16 @@ export async function handle(
}), }),
); );
// if dalle3 use openai api key // if dalle3 use openai api key
const baseUrl = req.headers.get("x-base-url"); const baseUrl = req.headers.get('x-base-url');
if (baseUrl?.includes("api.openai.com")) { if (baseUrl?.includes('api.openai.com')) {
if (!serverConfig.apiKey) { if (!serverConfig.apiKey) {
return NextResponse.json( return NextResponse.json(
{ error: "OpenAI API key not configured" }, { error: 'OpenAI API key not configured' },
{ status: 500 }, { status: 500 },
); );
}
headers.set("Authorization", `Bearer ${serverConfig.apiKey}`);
} }
headers.set('Authorization', `Bearer ${serverConfig.apiKey}`);
}
const controller = new AbortController(); const controller = new AbortController();
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
@@ -51,9 +52,9 @@ export async function handle(
method: req.method, method: req.method,
body: req.body, body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual", redirect: 'manual',
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
signal: controller.signal, signal: controller.signal,
}; };
@@ -68,15 +69,15 @@ export async function handle(
const res = await fetch(fetchUrl, fetchOptions); const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete('www-authenticate');
// to disable nginx buffering // 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 // 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 // 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 // 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 // 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, { return new Response(res.body, {
status: res.status, status: res.status,

View File

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

View File

@@ -1,9 +1,10 @@
import { getServerSideConfig } from "@/app/config/server"; import type { NextRequest } from 'next/server';
import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant"; import { auth } from '@/app/api/auth';
import { prettyObject } from "@/app/utils/format"; import { getServerSideConfig } from '@/app/config/server';
import { NextRequest, NextResponse } from "next/server"; import { ModelProvider, TENCENT_BASE_URL } from '@/app/constant';
import { auth } from "@/app/api/auth"; import { prettyObject } from '@/app/utils/format';
import { getHeader } from "@/app/utils/tencent"; import { getHeader } from '@/app/utils/tencent';
import { NextResponse } from 'next/server';
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
@@ -11,10 +12,10 @@ async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
console.log("[Tencent Route] params ", params); console.log('[Tencent Route] params ', params);
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const authResult = auth(req, ModelProvider.Hunyuan); const authResult = auth(req, ModelProvider.Hunyuan);
@@ -28,7 +29,7 @@ async function handle(
const response = await request(req); const response = await request(req);
return response; return response;
} catch (e) { } catch (e) {
console.error("[Tencent] ", e); console.error('[Tencent] ', e);
return NextResponse.json(prettyObject(e)); return NextResponse.json(prettyObject(e));
} }
} }
@@ -36,25 +37,25 @@ async function handle(
export const GET = handle; export const GET = handle;
export const POST = handle; export const POST = handle;
export const runtime = "edge"; export const runtime = 'edge';
export const preferredRegion = [ export const preferredRegion = [
"arn1", 'arn1',
"bom1", 'bom1',
"cdg1", 'cdg1',
"cle1", 'cle1',
"cpt1", 'cpt1',
"dub1", 'dub1',
"fra1", 'fra1',
"gru1", 'gru1',
"hnd1", 'hnd1',
"iad1", 'iad1',
"icn1", 'icn1',
"kix1", 'kix1',
"lhr1", 'lhr1',
"pdx1", 'pdx1',
"sfo1", 'sfo1',
"sin1", 'sin1',
"syd1", 'syd1',
]; ];
async function request(req: NextRequest) { async function request(req: NextRequest) {
@@ -62,15 +63,15 @@ async function request(req: NextRequest) {
let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL; let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
if (!baseUrl.startsWith("http")) { if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`; baseUrl = `https://${baseUrl}`;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Base Url]", baseUrl); console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => { () => {
@@ -91,9 +92,9 @@ async function request(req: NextRequest) {
headers, headers,
method: req.method, method: req.method,
body, body,
redirect: "manual", redirect: 'manual',
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
signal: controller.signal, signal: controller.signal,
}; };
@@ -102,9 +103,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete('www-authenticate');
// to disable nginx buffering // to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no"); newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, { return new Response(res.body, {
status: res.status, 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( async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { action: string; key: string[] } }, { params }: { params: { action: string; key: string[] } },
) { ) {
const requestUrl = new URL(req.url); const requestUrl = new URL(req.url);
const endpoint = requestUrl.searchParams.get("endpoint"); const endpoint = requestUrl.searchParams.get('endpoint');
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const [...key] = params.key; const [...key] = params.key;
// only allow to request to *.upstash.io // 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( return NextResponse.json(
{ {
error: true, error: true,
msg: "you are not allowed to request " + params.key.join("/"), msg: `you are not allowed to request ${params.key.join('/')}`,
}, },
{ {
status: 403, status: 403,
@@ -25,12 +26,12 @@ async function handle(
} }
// only allow upstash get and set method // only allow upstash get and set method
if (params.action !== "get" && params.action !== "set") { if (params.action !== 'get' && params.action !== 'set') {
console.log("[Upstash Route] forbidden action ", params.action); console.log('[Upstash Route] forbidden action ', params.action);
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
msg: "you are not allowed to request " + params.action, msg: `you are not allowed to request ${params.action}`,
}, },
{ {
status: 403, 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 method = req.method;
const shouldNotHaveBody = ["get", "head"].includes( const shouldNotHaveBody = ['get', 'head'].includes(
method?.toLowerCase() ?? "", method?.toLowerCase() ?? '',
); );
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
authorization: req.headers.get("authorization") ?? "", authorization: req.headers.get('authorization') ?? '',
}, },
body: shouldNotHaveBody ? null : req.body, body: shouldNotHaveBody ? null : req.body,
method, method,
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
}; };
console.log("[Upstash Proxy]", targetUrl, fetchOptions); console.log('[Upstash Proxy]', targetUrl, fetchOptions);
const fetchResult = await fetch(targetUrl, fetchOptions); const fetchResult = await fetch(targetUrl, fetchOptions);
console.log("[Any Proxy]", targetUrl, { console.log('[Any Proxy]', targetUrl, {
status: fetchResult.status, status: fetchResult.status,
statusText: fetchResult.statusText, statusText: fetchResult.statusText,
}); });
@@ -70,4 +71,4 @@ export const POST = handle;
export const GET = handle; export const GET = handle;
export const OPTIONS = 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 type { NextRequest } from 'next/server';
import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant"; import { getServerSideConfig } from '@/app/config/server';
import { getServerSideConfig } from "@/app/config/server"; import { NextResponse } from 'next/server';
import { internalAllowedWebDavEndpoints, STORAGE_KEY } from '../../../constant';
const config = getServerSideConfig(); const config = getServerSideConfig();
const mergedAllowedWebDavEndpoints = [ const mergedAllowedWebDavEndpoints = [
...internalAllowedWebDavEndpoints, ...internalAllowedWebDavEndpoints,
...config.allowedWebDavEndpoints, ...config.allowedWebDavEndpoints,
].filter((domain) => Boolean(domain.trim())); ].filter(domain => Boolean(domain.trim()));
const normalizeUrl = (url: string) => { function normalizeUrl(url: string) {
try { try {
return new URL(url); return new URL(url);
} catch (err) { } catch (err) {
return null; return null;
} }
}; }
async function handle( async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const folder = STORAGE_KEY; const folder = STORAGE_KEY;
const fileName = `${folder}/backup.json`; const fileName = `${folder}/backup.json`;
const requestUrl = new URL(req.url); const requestUrl = new URL(req.url);
let endpoint = requestUrl.searchParams.get("endpoint"); let endpoint = requestUrl.searchParams.get('endpoint');
let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method; const proxy_method = requestUrl.searchParams.get('proxy_method') || req.method;
// Validate the endpoint to prevent potential SSRF attacks // Validate the endpoint to prevent potential SSRF attacks
if ( if (
!endpoint || !endpoint
!mergedAllowedWebDavEndpoints.some((allowedEndpoint) => { || !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => {
const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint); const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
const normalizedEndpoint = normalizeUrl(endpoint as string); const normalizedEndpoint = normalizeUrl(endpoint as string);
return ( return (
normalizedEndpoint && normalizedEndpoint
normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname && && normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname
normalizedEndpoint.pathname.startsWith( && normalizedEndpoint.pathname.startsWith(
normalizedAllowedEndpoint.pathname, normalizedAllowedEndpoint.pathname,
) )
); );
@@ -50,7 +51,7 @@ async function handle(
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
msg: "Invalid endpoint", msg: 'Invalid endpoint',
}, },
{ {
status: 400, status: 400,
@@ -58,23 +59,23 @@ async function handle(
); );
} }
if (!endpoint?.endsWith("/")) { if (!endpoint?.endsWith('/')) {
endpoint += "/"; endpoint += '/';
} }
const endpointPath = params.path.join("/"); const endpointPath = params.path.join('/');
const targetPath = `${endpoint}${endpointPath}`; const targetPath = `${endpoint}${endpointPath}`;
// only allow MKCOL, GET, PUT // only allow MKCOL, GET, PUT
if ( if (
proxy_method !== "MKCOL" && proxy_method !== 'MKCOL'
proxy_method !== "GET" && && proxy_method !== 'GET'
proxy_method !== "PUT" && proxy_method !== 'PUT'
) { ) {
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
msg: "you are not allowed to request " + targetPath, msg: `you are not allowed to request ${targetPath}`,
}, },
{ {
status: 403, status: 403,
@@ -83,11 +84,11 @@ async function handle(
} }
// for MKCOL request, only allow request ${folder} // for MKCOL request, only allow request ${folder}
if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) { if (proxy_method === 'MKCOL' && !targetPath.endsWith(folder)) {
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
msg: "you are not allowed to request " + targetPath, msg: `you are not allowed to request ${targetPath}`,
}, },
{ {
status: 403, status: 403,
@@ -96,11 +97,11 @@ async function handle(
} }
// for GET request, only allow request ending with fileName // 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( return NextResponse.json(
{ {
error: true, error: true,
msg: "you are not allowed to request " + targetPath, msg: `you are not allowed to request ${targetPath}`,
}, },
{ {
status: 403, status: 403,
@@ -109,11 +110,11 @@ async function handle(
} }
// for PUT request, only allow request ending with fileName // 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( return NextResponse.json(
{ {
error: true, error: true,
msg: "you are not allowed to request " + targetPath, msg: `you are not allowed to request ${targetPath}`,
}, },
{ {
status: 403, status: 403,
@@ -124,19 +125,19 @@ async function handle(
const targetUrl = targetPath; const targetUrl = targetPath;
const method = proxy_method || req.method; const method = proxy_method || req.method;
const shouldNotHaveBody = ["get", "head"].includes( const shouldNotHaveBody = ['get', 'head'].includes(
method?.toLowerCase() ?? "", method?.toLowerCase() ?? '',
); );
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
authorization: req.headers.get("authorization") ?? "", authorization: req.headers.get('authorization') ?? '',
}, },
body: shouldNotHaveBody ? null : req.body, body: shouldNotHaveBody ? null : req.body,
redirect: "manual", redirect: 'manual',
method, method,
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
}; };
let fetchResult; let fetchResult;
@@ -145,10 +146,10 @@ async function handle(
fetchResult = await fetch(targetUrl, fetchOptions); fetchResult = await fetch(targetUrl, fetchOptions);
} finally { } finally {
console.log( console.log(
"[Any Proxy]", '[Any Proxy]',
targetUrl, targetUrl,
{ {
method: method, method,
}, },
{ {
status: fetchResult?.status, status: fetchResult?.status,
@@ -164,4 +165,4 @@ export const PUT = handle;
export const GET = handle; export const GET = handle;
export const OPTIONS = 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 { import {
XAI_BASE_URL,
ApiPath, ApiPath,
ModelProvider, ModelProvider,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; XAI_BASE_URL,
import { prettyObject } from "@/app/utils/format"; } from '@/app/constant';
import { NextRequest, NextResponse } from "next/server"; import { prettyObject } from '@/app/utils/format';
import { auth } from "@/app/api/auth"; import { isModelAvailableInServer } from '@/app/utils/model';
import { isModelAvailableInServer } from "@/app/utils/model"; import { NextResponse } from 'next/server';
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
@@ -16,10 +17,10 @@ export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
console.log("[XAI Route] params ", params); console.log('[XAI Route] params ', params);
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
return NextResponse.json({ body: "OK" }, { status: 200 }); return NextResponse.json({ body: 'OK' }, { status: 200 });
} }
const authResult = auth(req, ModelProvider.XAI); const authResult = auth(req, ModelProvider.XAI);
@@ -33,7 +34,7 @@ export async function handle(
const response = await request(req); const response = await request(req);
return response; return response;
} catch (e) { } catch (e) {
console.error("[XAI] ", e); console.error('[XAI] ', e);
return NextResponse.json(prettyObject(e)); return NextResponse.json(prettyObject(e));
} }
} }
@@ -42,20 +43,20 @@ async function request(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
// alibaba use base url or just remove the path // 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; let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL;
if (!baseUrl.startsWith("http")) { if (!baseUrl.startsWith('http')) {
baseUrl = `https://${baseUrl}`; baseUrl = `https://${baseUrl}`;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Proxy] ", path); console.log('[Proxy] ', path);
console.log("[Base Url]", baseUrl); console.log('[Base Url]', baseUrl);
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => { () => {
@@ -67,14 +68,14 @@ async function request(req: NextRequest) {
const fetchUrl = `${baseUrl}${path}`; const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
Authorization: req.headers.get("Authorization") ?? "", 'Authorization': req.headers.get('Authorization') ?? '',
}, },
method: req.method, method: req.method,
body: req.body, body: req.body,
redirect: "manual", redirect: 'manual',
// @ts-ignore // @ts-ignore
duplex: "half", duplex: 'half',
signal: controller.signal, signal: controller.signal,
}; };
@@ -113,9 +114,9 @@ async function request(req: NextRequest) {
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete('www-authenticate');
// to disable nginx buffering // to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no"); newHeaders.set('X-Accel-Buffering', 'no');
return new Response(res.body, { return new Response(res.body, {
status: res.status, 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 { import {
ACCESS_CODE_PREFIX, ACCESS_CODE_PREFIX,
ModelProvider, ModelProvider,
ServiceProvider, ServiceProvider,
} from "../constant"; } from '../constant';
import { import {
ChatMessageTool,
ChatMessage,
ModelType,
useAccessStore, useAccessStore,
useChatStore, useChatStore,
} from "../store"; } from '../store';
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai"; import { QwenApi } from './platforms/alibaba';
import { GeminiProApi } from "./platforms/google"; import { ClaudeApi } from './platforms/anthropic';
import { ClaudeApi } from "./platforms/anthropic"; import { ErnieApi } from './platforms/baidu';
import { ErnieApi } from "./platforms/baidu"; import { DoubaoApi } from './platforms/bytedance';
import { DoubaoApi } from "./platforms/bytedance"; import { ChatGLMApi } from './platforms/glm';
import { QwenApi } from "./platforms/alibaba"; import { GeminiProApi } from './platforms/google';
import { HunyuanApi } from "./platforms/tencent"; import { SparkApi } from './platforms/iflytek';
import { MoonshotApi } from "./platforms/moonshot"; import { MoonshotApi } from './platforms/moonshot';
import { SparkApi } from "./platforms/iflytek"; import { ChatGPTApi } from './platforms/openai';
import { XAIApi } from "./platforms/xai"; import { HunyuanApi } from './platforms/tencent';
import { ChatGLMApi } from "./platforms/glm"; import { XAIApi } from './platforms/xai';
export const ROLES = ["system", "user", "assistant"] as const; export const ROLES = ['system', 'user', 'assistant'] as const;
export type MessageRole = (typeof ROLES)[number]; export type MessageRole = (typeof ROLES)[number];
export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; export const Models = ['gpt-3.5-turbo', 'gpt-4'] as const;
export const TTSModels = ["tts-1", "tts-1-hd"] as const; export const TTSModels = ['tts-1', 'tts-1-hd'] as const;
export type ChatModel = ModelType; export type ChatModel = ModelType;
export interface MultimodalContent { export interface MultimodalContent {
type: "text" | "image_url"; type: 'text' | 'image_url';
text?: string; text?: string;
image_url?: { image_url?: {
url: string; url: string;
@@ -51,9 +54,9 @@ export interface LLMConfig {
stream?: boolean; stream?: boolean;
presence_penalty?: number; presence_penalty?: number;
frequency_penalty?: number; frequency_penalty?: number;
size?: DalleRequestPayload["size"]; size?: DalleRequestPayload['size'];
quality?: DalleRequestPayload["quality"]; quality?: DalleRequestPayload['quality'];
style?: DalleRequestPayload["style"]; style?: DalleRequestPayload['style'];
} }
export interface SpeechOptions { export interface SpeechOptions {
@@ -104,7 +107,7 @@ export abstract class LLMApi {
abstract models(): Promise<LLMModel[]>; abstract models(): Promise<LLMModel[]>;
} }
type ProviderName = "openai" | "azure" | "claude" | "palm"; type ProviderName = 'openai' | 'azure' | 'claude' | 'palm';
interface Model { interface Model {
name: string; name: string;
@@ -173,24 +176,24 @@ export class ClientApi {
async share(messages: ChatMessage[], avatarUrl: string | null = null) { async share(messages: ChatMessage[], avatarUrl: string | null = null) {
const msgs = messages const msgs = messages
.map((m) => ({ .map(m => ({
from: m.role === "user" ? "human" : "gpt", from: m.role === 'user' ? 'human' : 'gpt',
value: m.content, value: m.content,
})) }))
.concat([ .concat([
{ {
from: "human", from: 'human',
value: 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 // Please do not modify this message
console.log("[Share]", messages, msgs); console.log('[Share]', messages, msgs);
const clientConfig = getClientConfig(); const clientConfig = getClientConfig();
const proxyUrl = "/sharegpt"; const proxyUrl = '/sharegpt';
const rawUrl = "https://sharegpt.com/api/conversations"; const rawUrl = 'https://sharegpt.com/api/conversations';
const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl; const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl;
const res = await fetch(shareUrl, { const res = await fetch(shareUrl, {
body: JSON.stringify({ body: JSON.stringify({
@@ -198,13 +201,13 @@ export class ClientApi {
items: msgs, items: msgs,
}), }),
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
method: "POST", method: 'POST',
}); });
const resJson = await res.json(); const resJson = await res.json();
console.log("[Share]", resJson); console.log('[Share]', resJson);
if (resJson.id) { if (resJson.id) {
return `https://shareg.pt/${resJson.id}`; return `https://shareg.pt/${resJson.id}`;
} }
@@ -216,8 +219,8 @@ export function getBearerToken(
noBearer: boolean = false, noBearer: boolean = false,
): string { ): string {
return validString(apiKey) return validString(apiKey)
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}` ? `${noBearer ? '' : 'Bearer '}${apiKey.trim()}`
: ""; : '';
} }
export function validString(x: string): boolean { export function validString(x: string): boolean {
@@ -230,8 +233,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
let headers: Record<string, string> = {}; let headers: Record<string, string> = {};
if (!ignoreHeaders) { if (!ignoreHeaders) {
headers = { headers = {
"Content-Type": "application/json", 'Content-Type': 'application/json',
Accept: "application/json", 'Accept': 'application/json',
}; };
} }
@@ -253,24 +256,24 @@ export function getHeaders(ignoreHeaders: boolean = false) {
const apiKey = isGoogle const apiKey = isGoogle
? accessStore.googleApiKey ? accessStore.googleApiKey
: isAzure : isAzure
? accessStore.azureApiKey ? accessStore.azureApiKey
: isAnthropic : isAnthropic
? accessStore.anthropicApiKey ? accessStore.anthropicApiKey
: isByteDance : isByteDance
? accessStore.bytedanceApiKey ? accessStore.bytedanceApiKey
: isAlibaba : isAlibaba
? accessStore.alibabaApiKey ? accessStore.alibabaApiKey
: isMoonshot : isMoonshot
? accessStore.moonshotApiKey ? accessStore.moonshotApiKey
: isXAI : isXAI
? accessStore.xaiApiKey ? accessStore.xaiApiKey
: isChatGLM : isChatGLM
? accessStore.chatglmApiKey ? accessStore.chatglmApiKey
: isIflytek : isIflytek
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret ? `${accessStore.iflytekApiKey}:${accessStore.iflytekApiSecret}`
: "" : ''
: accessStore.openaiApiKey; : accessStore.openaiApiKey;
return { return {
isGoogle, isGoogle,
isAzure, isAzure,
@@ -289,12 +292,12 @@ export function getHeaders(ignoreHeaders: boolean = false) {
function getAuthHeader(): string { function getAuthHeader(): string {
return isAzure return isAzure
? "api-key" ? 'api-key'
: isAnthropic : isAnthropic
? "x-api-key" ? 'x-api-key'
: isGoogle : isGoogle
? "x-goog-api-key" ? 'x-goog-api-key'
: "Authorization"; : 'Authorization';
} }
const { const {
@@ -306,7 +309,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
isEnabledAccessControl, isEnabledAccessControl,
} = getConfig(); } = getConfig();
// when using baidu api in app, not set auth header // when using baidu api in app, not set auth header
if (isBaidu && clientConfig?.isApp) return headers; if (isBaidu && clientConfig?.isApp)
{ return headers; }
const authHeader = getAuthHeader(); const authHeader = getAuthHeader();
@@ -318,7 +322,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
if (bearerToken) { if (bearerToken) {
headers[authHeader] = bearerToken; headers[authHeader] = bearerToken;
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) { } else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
headers["Authorization"] = getBearerToken( headers.Authorization = getBearerToken(
ACCESS_CODE_PREFIX + accessStore.accessCode, ACCESS_CODE_PREFIX + accessStore.accessCode,
); );
} }

View File

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

View File

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

View File

@@ -1,34 +1,36 @@
import { Anthropic, ApiPath } from "@/app/constant"; import type {
import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api"; 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 { import {
useAccessStore, useAccessStore,
useAppConfig, useAppConfig,
useChatStore, useChatStore,
usePluginStore, usePluginStore,
ChatMessageTool, } from '@/app/store';
} from "@/app/store"; import { getMessageTextContent, isVisionModel } from '@/app/utils';
import { getClientConfig } from "@/app/config/client"; import { preProcessImageContent, stream } from '@/app/utils/chat';
import { ANTHROPIC_BASE_URL } from "@/app/constant"; import { cloudflareAIGatewayUrl } from '@/app/utils/cloudflare';
import { getMessageTextContent, isVisionModel } from "@/app/utils"; import { fetch } from '@/app/utils/stream';
import { preProcessImageContent, stream } from "@/app/utils/chat"; import { getHeaders } from '../api';
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export type MultiBlockContent = { export interface MultiBlockContent {
type: "image" | "text"; type: 'image' | 'text';
source?: { source?: {
type: string; type: string;
media_type: string; media_type: string;
data: string; data: string;
}; };
text?: string; text?: string;
}; }
export type AnthropicMessage = { export interface AnthropicMessage {
role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper]; role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper];
content: string | MultiBlockContent[]; content: string | MultiBlockContent[];
}; }
export interface AnthropicChatRequest { export interface AnthropicChatRequest {
model: string; // The model that will complete your prompt. model: string; // The model that will complete your prompt.
@@ -56,7 +58,7 @@ export interface ChatRequest {
export interface ChatResponse { export interface ChatResponse {
completion: string; completion: string;
stop_reason: "stop_sequence" | "max_tokens"; stop_reason: 'stop_sequence' | 'max_tokens';
model: string; model: string;
} }
@@ -66,23 +68,24 @@ export type ChatStreamResponse = ChatResponse & {
}; };
const ClaudeMapper = { const ClaudeMapper = {
assistant: "assistant", assistant: 'assistant',
user: "user", user: 'user',
system: "user", system: 'user',
} as const; } as const;
const keys = ["claude-2, claude-instant-1"]; const keys = ['claude-2, claude-instant-1'];
export class ClaudeApi implements LLMApi { export class ClaudeApi implements LLMApi {
speech(options: SpeechOptions): Promise<ArrayBuffer> { speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented."); throw new Error('Method not implemented.');
} }
extractMessage(res: any) { extractMessage(res: any) {
console.log("[Response] claude response: ", res); console.log('[Response] claude response: ', res);
return res?.content?.[0]?.text; return res?.content?.[0]?.text;
} }
async chat(options: ChatOptions): Promise<void> { async chat(options: ChatOptions): Promise<void> {
const visionModel = isVisionModel(options.config.model); const visionModel = isVisionModel(options.config.model);
@@ -99,13 +102,13 @@ export class ClaudeApi implements LLMApi {
}; };
// try get base64image from local cache image_url // try get base64image from local cache image_url
const messages: ChatOptions["messages"] = []; const messages: ChatOptions['messages'] = [];
for (const v of options.messages) { for (const v of options.messages) {
const content = await preProcessImageContent(v.content); const content = await preProcessImageContent(v.content);
messages.push({ role: v.role, 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 // 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++) { for (let i = 0; i < messages.length - 1; i++) {
@@ -116,8 +119,8 @@ export class ClaudeApi implements LLMApi {
messages[i] = [ messages[i] = [
message, message,
{ {
role: "assistant", role: 'assistant',
content: ";", content: ';',
}, },
] as any; ] as any;
} }
@@ -126,15 +129,17 @@ export class ClaudeApi implements LLMApi {
const prompt = messages const prompt = messages
.flat() .flat()
.filter((v) => { .filter((v) => {
if (!v.content) return false; if (!v.content)
if (typeof v.content === "string" && !v.content.trim()) return false; { return false; }
if (typeof v.content === 'string' && !v.content.trim())
{ return false; }
return true; return true;
}) })
.map((v) => { .map((v) => {
const { role, content } = 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 { return {
role: insideRole, role: insideRole,
content: getMessageTextContent(v), content: getMessageTextContent(v),
@@ -143,25 +148,25 @@ export class ClaudeApi implements LLMApi {
return { return {
role: insideRole, role: insideRole,
content: content content: content
.filter((v) => v.image_url || v.text) .filter(v => v.image_url || v.text)
.map(({ type, text, image_url }) => { .map(({ type, text, image_url }) => {
if (type === "text") { if (type === 'text') {
return { return {
type, type,
text: text!, text: text!,
}; };
} }
const { url = "" } = image_url || {}; const { url = '' } = image_url || {};
const colonIndex = url.indexOf(":"); const colonIndex = url.indexOf(':');
const semicolonIndex = url.indexOf(";"); const semicolonIndex = url.indexOf(';');
const comma = url.indexOf(","); const comma = url.indexOf(',');
const mimeType = url.slice(colonIndex + 1, semicolonIndex); const mimeType = url.slice(colonIndex + 1, semicolonIndex);
const encodeType = url.slice(semicolonIndex + 1, comma); const encodeType = url.slice(semicolonIndex + 1, comma);
const data = url.slice(comma + 1); const data = url.slice(comma + 1);
return { return {
type: "image" as const, type: 'image' as const,
source: { source: {
type: encodeType, type: encodeType,
media_type: mimeType, media_type: mimeType,
@@ -172,10 +177,10 @@ export class ClaudeApi implements LLMApi {
}; };
}); });
if (prompt[0]?.role === "assistant") { if (prompt[0]?.role === 'assistant') {
prompt.unshift({ prompt.unshift({
role: "user", role: 'user',
content: ";", content: ';',
}); });
} }
@@ -208,10 +213,10 @@ export class ClaudeApi implements LLMApi {
requestBody, requestBody,
{ {
...getHeaders(), ...getHeaders(),
"anthropic-version": accessStore.anthropicApiVersion, 'anthropic-version': accessStore.anthropicApiVersion,
}, },
// @ts-ignore // @ts-ignore
tools.map((tool) => ({ tools.map(tool => ({
name: tool?.function?.name, name: tool?.function?.name,
description: tool?.function?.description, description: tool?.function?.description,
input_schema: tool?.function?.parameters, input_schema: tool?.function?.parameters,
@@ -224,41 +229,41 @@ export class ClaudeApi implements LLMApi {
let chunkJson: let chunkJson:
| undefined | undefined
| { | {
type: "content_block_delta" | "content_block_stop"; type: 'content_block_delta' | 'content_block_stop';
content_block?: { content_block?: {
type: "tool_use"; type: 'tool_use';
id: string; id: string;
name: string; name: string;
};
delta?: {
type: "text_delta" | "input_json_delta";
text?: string;
partial_json?: string;
};
index: number;
}; };
delta?: {
type: 'text_delta' | 'input_json_delta';
text?: string;
partial_json?: string;
};
index: number;
};
chunkJson = JSON.parse(text); chunkJson = JSON.parse(text);
if (chunkJson?.content_block?.type == "tool_use") { if (chunkJson?.content_block?.type == 'tool_use') {
index += 1; index += 1;
const id = chunkJson?.content_block.id; const id = chunkJson?.content_block.id;
const name = chunkJson?.content_block.name; const name = chunkJson?.content_block.name;
runTools.push({ runTools.push({
id, id,
type: "function", type: 'function',
function: { function: {
name, name,
arguments: "", arguments: '',
}, },
}); });
} }
if ( if (
chunkJson?.delta?.type == "input_json_delta" && chunkJson?.delta?.type == 'input_json_delta'
chunkJson?.delta?.partial_json && chunkJson?.delta?.partial_json
) { ) {
// @ts-ignore // @ts-ignore
runTools[index]["function"]["arguments"] += runTools[index].function.arguments
chunkJson?.delta?.partial_json; += chunkJson?.delta?.partial_json;
} }
return chunkJson?.delta?.text; return chunkJson?.delta?.text;
}, },
@@ -276,10 +281,10 @@ export class ClaudeApi implements LLMApi {
requestPayload?.messages?.length, requestPayload?.messages?.length,
0, 0,
{ {
role: "assistant", role: 'assistant',
content: toolCallMessage.tool_calls.map( content: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({ (tool: ChatMessageTool) => ({
type: "tool_use", type: 'tool_use',
id: tool.id, id: tool.id,
name: tool?.function?.name, name: tool?.function?.name,
input: tool?.function?.arguments input: tool?.function?.arguments
@@ -289,11 +294,11 @@ export class ClaudeApi implements LLMApi {
), ),
}, },
// @ts-ignore // @ts-ignore
...toolCallResult.map((result) => ({ ...toolCallResult.map(result => ({
role: "user", role: 'user',
content: [ content: [
{ {
type: "tool_result", type: 'tool_result',
tool_use_id: result.tool_call_id, tool_use_id: result.tool_call_id,
content: result.content, content: result.content,
}, },
@@ -305,12 +310,12 @@ export class ClaudeApi implements LLMApi {
); );
} else { } else {
const payload = { const payload = {
method: "POST", method: 'POST',
body: JSON.stringify(requestBody), body: JSON.stringify(requestBody),
signal: controller.signal, signal: controller.signal,
headers: { headers: {
...getHeaders(), // get common headers ...getHeaders(), // get common headers
"anthropic-version": accessStore.anthropicApiVersion, 'anthropic-version': accessStore.anthropicApiVersion,
// do not send `anthropicApiKey` in browser!!! // do not send `anthropicApiKey` in browser!!!
// Authorization: getAuthKey(accessStore.anthropicApiKey), // Authorization: getAuthKey(accessStore.anthropicApiKey),
}, },
@@ -318,7 +323,7 @@ export class ClaudeApi implements LLMApi {
try { try {
controller.signal.onabort = () => controller.signal.onabort = () =>
options.onFinish("", new Response(null, { status: 400 })); options.onFinish('', new Response(null, { status: 400 }));
const res = await fetch(path, payload); const res = await fetch(path, payload);
const resJson = await res.json(); const resJson = await res.json();
@@ -326,17 +331,19 @@ export class ClaudeApi implements LLMApi {
const message = this.extractMessage(resJson); const message = this.extractMessage(resJson);
options.onFinish(message, res); options.onFinish(message, res);
} catch (e) { } catch (e) {
console.error("failed to chat", e); console.error('failed to chat', e);
options.onError?.(e as Error); options.onError?.(e as Error);
} }
} }
} }
async usage() { async usage() {
return { return {
used: 0, used: 0,
total: 0, total: 0,
}; };
} }
async models() { async models() {
// const provider = { // const provider = {
// id: "anthropic", // id: "anthropic",
@@ -377,10 +384,11 @@ export class ClaudeApi implements LLMApi {
// }, // },
]; ];
} }
path(path: string): string { path(path: string): string {
const accessStore = useAccessStore.getState(); const accessStore = useAccessStore.getState();
let baseUrl: string = ""; let baseUrl: string = '';
if (accessStore.useCustomConfig) { if (accessStore.useCustomConfig) {
baseUrl = accessStore.anthropicUrl; baseUrl = accessStore.anthropicUrl;
@@ -393,19 +401,20 @@ export class ClaudeApi implements LLMApi {
baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic; baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
} }
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) { if (!baseUrl.startsWith('http') && !baseUrl.startsWith('/api')) {
baseUrl = "https://" + baseUrl; baseUrl = `https://${baseUrl}`;
} }
baseUrl = trimEnd(baseUrl, "/"); baseUrl = trimEnd(baseUrl, '/');
// try rebuild url, when using cloudflare ai gateway in client // try rebuild url, when using cloudflare ai gateway in client
return cloudflareAIGatewayUrl(`${baseUrl}/${path}`); return cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
} }
} }
function trimEnd(s: string, end = " ") { function trimEnd(s: string, end = ' ') {
if (end.length === 0) return s; if (end.length === 0)
{ return s; }
while (s.endsWith(end)) { while (s.endsWith(end)) {
s = s.slice(0, -end.length); 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 { import {
ApiPath, ApiPath,
Baidu, Baidu,
BAIDU_BASE_URL, BAIDU_BASE_URL,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
} from "@/app/constant"; } from '@/app/constant';
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { getAccessToken } from "@/app/utils/baidu";
import { import { useAccessStore, useAppConfig, useChatStore } from '@/app/store';
ChatOptions, import { getMessageTextContent } from '@/app/utils';
getHeaders, import { getAccessToken } from '@/app/utils/baidu';
LLMApi, import { prettyObject } from '@/app/utils/format';
LLMModel, import { fetch } from '@/app/utils/stream';
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import { import {
EventStreamContentType, EventStreamContentType,
fetchEventSource, fetchEventSource,
} from "@fortaine/fetch-event-source"; } from '@fortaine/fetch-event-source';
import { prettyObject } from "@/app/utils/format"; import Locale from '../../locales';
import { getClientConfig } from "@/app/config/client"; import {
import { getMessageTextContent } from "@/app/utils"; getHeaders,
import { fetch } from "@/app/utils/stream"; } from '../api';
export interface OpenAIListModelResponse { export interface OpenAIListModelResponse {
object: string; object: string;
@@ -37,7 +39,7 @@ export interface OpenAIListModelResponse {
interface RequestPayload { interface RequestPayload {
messages: { messages: {
role: "system" | "user" | "assistant"; role: 'system' | 'user' | 'assistant';
content: string | MultimodalContent[]; content: string | MultimodalContent[];
}[]; }[];
stream?: boolean; stream?: boolean;
@@ -53,7 +55,7 @@ export class ErnieApi implements LLMApi {
path(path: string): string { path(path: string): string {
const accessStore = useAccessStore.getState(); const accessStore = useAccessStore.getState();
let baseUrl = ""; let baseUrl = '';
if (accessStore.useCustomConfig) { if (accessStore.useCustomConfig) {
baseUrl = accessStore.baiduUrl; baseUrl = accessStore.baiduUrl;
@@ -65,40 +67,40 @@ export class ErnieApi implements LLMApi {
baseUrl = isApp ? BAIDU_BASE_URL : ApiPath.Baidu; baseUrl = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1); baseUrl = baseUrl.slice(0, baseUrl.length - 1);
} }
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Baidu)) { if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Baidu)) {
baseUrl = "https://" + baseUrl; 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> { speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented."); throw new Error('Method not implemented.');
} }
async chat(options: ChatOptions) { async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({ const messages = options.messages.map(v => ({
// "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function", // "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), content: getMessageTextContent(v),
})); }));
// "error_code": 336006, "error_msg": "the length of messages must be an odd number", // "error_code": 336006, "error_msg": "the length of messages must be an odd number",
if (messages.length % 2 === 0) { if (messages.length % 2 === 0) {
if (messages.at(0)?.role === "user") { if (messages.at(0)?.role === 'user') {
messages.splice(1, 0, { messages.splice(1, 0, {
role: "assistant", role: 'assistant',
content: " ", content: ' ',
}); });
} else { } else {
messages.unshift({ messages.unshift({
role: "user", role: 'user',
content: " ", content: ' ',
}); });
} }
} }
@@ -122,7 +124,7 @@ export class ErnieApi implements LLMApi {
top_p: modelConfig.top_p, top_p: modelConfig.top_p,
}; };
console.log("[Request] Baidu payload: ", requestPayload); console.log('[Request] Baidu payload: ', requestPayload);
const controller = new AbortController(); const controller = new AbortController();
options.onController?.(controller); options.onController?.(controller);
@@ -131,7 +133,7 @@ export class ErnieApi implements LLMApi {
let chatPath = this.path(Baidu.ChatPath(modelConfig.model)); let chatPath = this.path(Baidu.ChatPath(modelConfig.model));
// getAccessToken can not run in browser, because cors error // getAccessToken can not run in browser, because cors error
if (!!getClientConfig()?.isApp) { if (getClientConfig()?.isApp) {
const accessStore = useAccessStore.getState(); const accessStore = useAccessStore.getState();
if (accessStore.useCustomConfig) { if (accessStore.useCustomConfig) {
if (accessStore.isValidBaidu()) { if (accessStore.isValidBaidu()) {
@@ -140,13 +142,13 @@ export class ErnieApi implements LLMApi {
accessStore.baiduSecretKey, accessStore.baiduSecretKey,
); );
chatPath = `${chatPath}${ chatPath = `${chatPath}${
chatPath.includes("?") ? "&" : "?" chatPath.includes('?') ? '&' : '?'
}access_token=${access_token}`; }access_token=${access_token}`;
} }
} }
} }
const chatPayload = { const chatPayload = {
method: "POST", method: 'POST',
body: JSON.stringify(requestPayload), body: JSON.stringify(requestPayload),
signal: controller.signal, signal: controller.signal,
headers: getHeaders(), headers: getHeaders(),
@@ -159,8 +161,8 @@ export class ErnieApi implements LLMApi {
); );
if (shouldStream) { if (shouldStream) {
let responseText = ""; let responseText = '';
let remainText = ""; let remainText = '';
let finished = false; let finished = false;
let responseRes: Response; let responseRes: Response;
@@ -168,9 +170,9 @@ export class ErnieApi implements LLMApi {
function animateResponseText() { function animateResponseText() {
if (finished || controller.signal.aborted) { if (finished || controller.signal.aborted) {
responseText += remainText; responseText += remainText;
console.log("[Response Animation] finished"); console.log('[Response Animation] finished');
if (responseText?.length === 0) { if (responseText?.length === 0) {
options.onError?.(new Error("empty response from server")); options.onError?.(new Error('empty response from server'));
} }
return; return;
} }
@@ -203,20 +205,20 @@ export class ErnieApi implements LLMApi {
...chatPayload, ...chatPayload,
async onopen(res) { async onopen(res) {
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type"); const contentType = res.headers.get('content-type');
console.log("[Baidu] request response content type: ", contentType); console.log('[Baidu] request response content type: ', contentType);
responseRes = res; responseRes = res;
if (contentType?.startsWith("text/plain")) { if (contentType?.startsWith('text/plain')) {
responseText = await res.clone().text(); responseText = await res.clone().text();
return finish(); return finish();
} }
if ( if (
!res.ok || !res.ok
!res.headers || !res.headers
.get("content-type") .get('content-type')
?.startsWith(EventStreamContentType) || ?.startsWith(EventStreamContentType)
res.status !== 200 || res.status !== 200
) { ) {
const responseTexts = [responseText]; const responseTexts = [responseText];
let extraInfo = await res.clone().text(); let extraInfo = await res.clone().text();
@@ -233,13 +235,13 @@ export class ErnieApi implements LLMApi {
responseTexts.push(extraInfo); responseTexts.push(extraInfo);
} }
responseText = responseTexts.join("\n\n"); responseText = responseTexts.join('\n\n');
return finish(); return finish();
} }
}, },
onmessage(msg) { onmessage(msg) {
if (msg.data === "[DONE]" || finished) { if (msg.data === '[DONE]' || finished) {
return finish(); return finish();
} }
const text = msg.data; const text = msg.data;
@@ -250,7 +252,7 @@ export class ErnieApi implements LLMApi {
remainText += delta; remainText += delta;
} }
} catch (e) { } catch (e) {
console.error("[Request] parse error", text, msg); console.error('[Request] parse error', text, msg);
} }
}, },
onclose() { onclose() {
@@ -271,10 +273,11 @@ export class ErnieApi implements LLMApi {
options.onFinish(message, res); options.onFinish(message, res);
} }
} catch (e) { } 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); options.onError?.(e as Error);
} }
} }
async usage() { async usage() {
return { return {
used: 0, 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 { import {
ApiPath, ApiPath,
ByteDance, ByteDance,
BYTEDANCE_BASE_URL, BYTEDANCE_BASE_URL,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
} from "@/app/constant"; } from '@/app/constant';
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { useAccessStore, useAppConfig, useChatStore } from '@/app/store';
import { getMessageTextContent } from '@/app/utils';
import { import { prettyObject } from '@/app/utils/format';
ChatOptions, import { fetch } from '@/app/utils/stream';
getHeaders,
LLMApi,
LLMModel,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import { import {
EventStreamContentType, EventStreamContentType,
fetchEventSource, fetchEventSource,
} from "@fortaine/fetch-event-source"; } from '@fortaine/fetch-event-source';
import { prettyObject } from "@/app/utils/format"; import Locale from '../../locales';
import { getClientConfig } from "@/app/config/client"; import {
import { getMessageTextContent } from "@/app/utils"; getHeaders,
import { fetch } from "@/app/utils/stream"; } from '../api';
export interface OpenAIListModelResponse { export interface OpenAIListModelResponse {
object: string; object: string;
@@ -36,7 +38,7 @@ export interface OpenAIListModelResponse {
interface RequestPayload { interface RequestPayload {
messages: { messages: {
role: "system" | "user" | "assistant"; role: 'system' | 'user' | 'assistant';
content: string | MultimodalContent[]; content: string | MultimodalContent[];
}[]; }[];
stream?: boolean; stream?: boolean;
@@ -52,7 +54,7 @@ export class DoubaoApi implements LLMApi {
path(path: string): string { path(path: string): string {
const accessStore = useAccessStore.getState(); const accessStore = useAccessStore.getState();
let baseUrl = ""; let baseUrl = '';
if (accessStore.useCustomConfig) { if (accessStore.useCustomConfig) {
baseUrl = accessStore.bytedanceUrl; baseUrl = accessStore.bytedanceUrl;
@@ -63,28 +65,28 @@ export class DoubaoApi implements LLMApi {
baseUrl = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance; baseUrl = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1); baseUrl = baseUrl.slice(0, baseUrl.length - 1);
} }
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ByteDance)) { if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.ByteDance)) {
baseUrl = "https://" + baseUrl; 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) { extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? ""; return res.choices?.at(0)?.message?.content ?? '';
} }
speech(options: SpeechOptions): Promise<ArrayBuffer> { speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented."); throw new Error('Method not implemented.');
} }
async chat(options: ChatOptions) { async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({ const messages = options.messages.map(v => ({
role: v.role, role: v.role,
content: getMessageTextContent(v), content: getMessageTextContent(v),
})); }));
@@ -114,7 +116,7 @@ export class DoubaoApi implements LLMApi {
try { try {
const chatPath = this.path(ByteDance.ChatPath); const chatPath = this.path(ByteDance.ChatPath);
const chatPayload = { const chatPayload = {
method: "POST", method: 'POST',
body: JSON.stringify(requestPayload), body: JSON.stringify(requestPayload),
signal: controller.signal, signal: controller.signal,
headers: getHeaders(), headers: getHeaders(),
@@ -127,8 +129,8 @@ export class DoubaoApi implements LLMApi {
); );
if (shouldStream) { if (shouldStream) {
let responseText = ""; let responseText = '';
let remainText = ""; let remainText = '';
let finished = false; let finished = false;
let responseRes: Response; let responseRes: Response;
@@ -136,9 +138,9 @@ export class DoubaoApi implements LLMApi {
function animateResponseText() { function animateResponseText() {
if (finished || controller.signal.aborted) { if (finished || controller.signal.aborted) {
responseText += remainText; responseText += remainText;
console.log("[Response Animation] finished"); console.log('[Response Animation] finished');
if (responseText?.length === 0) { if (responseText?.length === 0) {
options.onError?.(new Error("empty response from server")); options.onError?.(new Error('empty response from server'));
} }
return; return;
} }
@@ -171,23 +173,23 @@ export class DoubaoApi implements LLMApi {
...chatPayload, ...chatPayload,
async onopen(res) { async onopen(res) {
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type"); const contentType = res.headers.get('content-type');
console.log( console.log(
"[ByteDance] request response content type: ", '[ByteDance] request response content type: ',
contentType, contentType,
); );
responseRes = res; responseRes = res;
if (contentType?.startsWith("text/plain")) { if (contentType?.startsWith('text/plain')) {
responseText = await res.clone().text(); responseText = await res.clone().text();
return finish(); return finish();
} }
if ( if (
!res.ok || !res.ok
!res.headers || !res.headers
.get("content-type") .get('content-type')
?.startsWith(EventStreamContentType) || ?.startsWith(EventStreamContentType)
res.status !== 200 || res.status !== 200
) { ) {
const responseTexts = [responseText]; const responseTexts = [responseText];
let extraInfo = await res.clone().text(); let extraInfo = await res.clone().text();
@@ -204,13 +206,13 @@ export class DoubaoApi implements LLMApi {
responseTexts.push(extraInfo); responseTexts.push(extraInfo);
} }
responseText = responseTexts.join("\n\n"); responseText = responseTexts.join('\n\n');
return finish(); return finish();
} }
}, },
onmessage(msg) { onmessage(msg) {
if (msg.data === "[DONE]" || finished) { if (msg.data === '[DONE]' || finished) {
return finish(); return finish();
} }
const text = msg.data; const text = msg.data;
@@ -224,7 +226,7 @@ export class DoubaoApi implements LLMApi {
remainText += delta; remainText += delta;
} }
} catch (e) { } catch (e) {
console.error("[Request] parse error", text, msg); console.error('[Request] parse error', text, msg);
} }
}, },
onclose() { onclose() {
@@ -245,10 +247,11 @@ export class DoubaoApi implements LLMApi {
options.onFinish(message, res); options.onFinish(message, res);
} }
} catch (e) { } 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); options.onError?.(e as Error);
} }
} }
async usage() { async usage() {
return { return {
used: 0, 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 { import {
ApiPath, ApiPath,
CHATGLM_BASE_URL,
ChatGLM, ChatGLM,
CHATGLM_BASE_URL,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
} from "@/app/constant"; } from '@/app/constant';
import { import {
useAccessStore, useAccessStore,
useAppConfig, useAppConfig,
useChatStore, useChatStore,
ChatMessageTool,
usePluginStore, usePluginStore,
} from "@/app/store"; } from '@/app/store';
import { stream } from "@/app/utils/chat"; import { getMessageTextContent } from '@/app/utils';
import { stream } from '@/app/utils/chat';
import { fetch } from '@/app/utils/stream';
import { import {
ChatOptions,
getHeaders, getHeaders,
LLMApi, } from '../api';
LLMModel,
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export class ChatGLMApi implements LLMApi { export class ChatGLMApi implements LLMApi {
private disableListModels = true; private disableListModels = true;
@@ -31,7 +35,7 @@ export class ChatGLMApi implements LLMApi {
path(path: string): string { path(path: string): string {
const accessStore = useAccessStore.getState(); const accessStore = useAccessStore.getState();
let baseUrl = ""; let baseUrl = '';
if (accessStore.useCustomConfig) { if (accessStore.useCustomConfig) {
baseUrl = accessStore.chatglmUrl; baseUrl = accessStore.chatglmUrl;
@@ -43,28 +47,28 @@ export class ChatGLMApi implements LLMApi {
baseUrl = isApp ? CHATGLM_BASE_URL : apiPath; baseUrl = isApp ? CHATGLM_BASE_URL : apiPath;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1); baseUrl = baseUrl.slice(0, baseUrl.length - 1);
} }
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) { if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.ChatGLM)) {
baseUrl = "https://" + baseUrl; 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) { extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? ""; return res.choices?.at(0)?.message?.content ?? '';
} }
speech(options: SpeechOptions): Promise<ArrayBuffer> { speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented."); throw new Error('Method not implemented.');
} }
async chat(options: ChatOptions) { async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = []; const messages: ChatOptions['messages'] = [];
for (const v of options.messages) { for (const v of options.messages) {
const content = getMessageTextContent(v); const content = getMessageTextContent(v);
messages.push({ role: v.role, content }); messages.push({ role: v.role, content });
@@ -89,7 +93,7 @@ export class ChatGLMApi implements LLMApi {
top_p: modelConfig.top_p, top_p: modelConfig.top_p,
}; };
console.log("[Request] glm payload: ", requestPayload); console.log('[Request] glm payload: ', requestPayload);
const shouldStream = !!options.config.stream; const shouldStream = !!options.config.stream;
const controller = new AbortController(); const controller = new AbortController();
@@ -98,7 +102,7 @@ export class ChatGLMApi implements LLMApi {
try { try {
const chatPath = this.path(ChatGLM.ChatPath); const chatPath = this.path(ChatGLM.ChatPath);
const chatPayload = { const chatPayload = {
method: "POST", method: 'POST',
body: JSON.stringify(requestPayload), body: JSON.stringify(requestPayload),
signal: controller.signal, signal: controller.signal,
headers: getHeaders(), headers: getHeaders(),
@@ -149,7 +153,7 @@ export class ChatGLMApi implements LLMApi {
}); });
} else { } else {
// @ts-ignore // @ts-ignore
runTools[index]["function"]["arguments"] += args; runTools[index].function.arguments += args;
} }
} }
return choices[0]?.delta?.content; return choices[0]?.delta?.content;
@@ -180,10 +184,11 @@ export class ChatGLMApi implements LLMApi {
options.onFinish(message, res); options.onFinish(message, res);
} }
} catch (e) { } 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); options.onError?.(e as Error);
} }
} }
async usage() { async usage() {
return { return {
used: 0, used: 0,

View File

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

View File

@@ -1,30 +1,32 @@
"use client"; 'use client';
import { import type {
ApiPath,
IFLYTEK_BASE_URL,
Iflytek,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
ChatOptions, ChatOptions,
getHeaders,
LLMApi, LLMApi,
LLMModel, LLMModel,
SpeechOptions, SpeechOptions,
} from "../api"; } from '../api';
import Locale from "../../locales"; 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 { import {
EventStreamContentType, EventStreamContentType,
fetchEventSource, fetchEventSource,
} from "@fortaine/fetch-event-source"; } from '@fortaine/fetch-event-source';
import { prettyObject } from "@/app/utils/format"; import Locale from '../../locales';
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
import { RequestPayload } from "./openai"; import {
getHeaders,
} from '../api';
export class SparkApi implements LLMApi { export class SparkApi implements LLMApi {
private disableListModels = true; private disableListModels = true;
@@ -32,7 +34,7 @@ export class SparkApi implements LLMApi {
path(path: string): string { path(path: string): string {
const accessStore = useAccessStore.getState(); const accessStore = useAccessStore.getState();
let baseUrl = ""; let baseUrl = '';
if (accessStore.useCustomConfig) { if (accessStore.useCustomConfig) {
baseUrl = accessStore.iflytekUrl; baseUrl = accessStore.iflytekUrl;
@@ -44,28 +46,28 @@ export class SparkApi implements LLMApi {
baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath; baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1); baseUrl = baseUrl.slice(0, baseUrl.length - 1);
} }
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) { if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Iflytek)) {
baseUrl = "https://" + baseUrl; 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) { extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? ""; return res.choices?.at(0)?.message?.content ?? '';
} }
speech(options: SpeechOptions): Promise<ArrayBuffer> { speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented."); throw new Error('Method not implemented.');
} }
async chat(options: ChatOptions) { async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = []; const messages: ChatOptions['messages'] = [];
for (const v of options.messages) { for (const v of options.messages) {
const content = getMessageTextContent(v); const content = getMessageTextContent(v);
messages.push({ role: v.role, content }); 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. // 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 shouldStream = !!options.config.stream;
const controller = new AbortController(); const controller = new AbortController();
@@ -101,7 +103,7 @@ export class SparkApi implements LLMApi {
try { try {
const chatPath = this.path(Iflytek.ChatPath); const chatPath = this.path(Iflytek.ChatPath);
const chatPayload = { const chatPayload = {
method: "POST", method: 'POST',
body: JSON.stringify(requestPayload), body: JSON.stringify(requestPayload),
signal: controller.signal, signal: controller.signal,
headers: getHeaders(), headers: getHeaders(),
@@ -114,8 +116,8 @@ export class SparkApi implements LLMApi {
); );
if (shouldStream) { if (shouldStream) {
let responseText = ""; let responseText = '';
let remainText = ""; let remainText = '';
let finished = false; let finished = false;
let responseRes: Response; let responseRes: Response;
@@ -123,7 +125,7 @@ export class SparkApi implements LLMApi {
function animateResponseText() { function animateResponseText() {
if (finished || controller.signal.aborted) { if (finished || controller.signal.aborted) {
responseText += remainText; responseText += remainText;
console.log("[Response Animation] finished"); console.log('[Response Animation] finished');
return; return;
} }
@@ -155,21 +157,21 @@ export class SparkApi implements LLMApi {
...chatPayload, ...chatPayload,
async onopen(res) { async onopen(res) {
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type"); const contentType = res.headers.get('content-type');
console.log("[Spark] request response content type: ", contentType); console.log('[Spark] request response content type: ', contentType);
responseRes = res; responseRes = res;
if (contentType?.startsWith("text/plain")) { if (contentType?.startsWith('text/plain')) {
responseText = await res.clone().text(); responseText = await res.clone().text();
return finish(); return finish();
} }
// Handle different error scenarios // Handle different error scenarios
if ( if (
!res.ok || !res.ok
!res.headers || !res.headers
.get("content-type") .get('content-type')
?.startsWith(EventStreamContentType) || ?.startsWith(EventStreamContentType)
res.status !== 200 || res.status !== 200
) { ) {
let extraInfo = await res.clone().text(); let extraInfo = await res.clone().text();
try { try {
@@ -190,7 +192,7 @@ export class SparkApi implements LLMApi {
} }
}, },
onmessage(msg) { onmessage(msg) {
if (msg.data === "[DONE]" || finished) { if (msg.data === '[DONE]' || finished) {
return finish(); return finish();
} }
const text = msg.data; const text = msg.data;
@@ -205,7 +207,7 @@ export class SparkApi implements LLMApi {
remainText += delta; remainText += delta;
} }
} catch (e) { } catch (e) {
console.error("[Request] parse error", text); console.error('[Request] parse error', text);
options.onError?.(new Error(`Failed to parse response: ${text}`)); options.onError?.(new Error(`Failed to parse response: ${text}`));
} }
}, },
@@ -235,7 +237,7 @@ export class SparkApi implements LLMApi {
options.onFinish(message, res); options.onFinish(message, res);
} }
} catch (e) { } 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); 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. // azure and openai, using same models. so using same LLMApi.
import { import {
ApiPath, ApiPath,
MOONSHOT_BASE_URL,
Moonshot, Moonshot,
MOONSHOT_BASE_URL,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
} from "@/app/constant"; } from '@/app/constant';
import { import {
useAccessStore, useAccessStore,
useAppConfig, useAppConfig,
useChatStore, useChatStore,
ChatMessageTool,
usePluginStore, usePluginStore,
} from "@/app/store"; } from '@/app/store';
import { stream } from "@/app/utils/chat"; import { getMessageTextContent } from '@/app/utils';
import { stream } from '@/app/utils/chat';
import { fetch } from '@/app/utils/stream';
import { import {
ChatOptions,
getHeaders, getHeaders,
LLMApi, } from '../api';
LLMModel,
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export class MoonshotApi implements LLMApi { export class MoonshotApi implements LLMApi {
private disableListModels = true; private disableListModels = true;
@@ -32,7 +36,7 @@ export class MoonshotApi implements LLMApi {
path(path: string): string { path(path: string): string {
const accessStore = useAccessStore.getState(); const accessStore = useAccessStore.getState();
let baseUrl = ""; let baseUrl = '';
if (accessStore.useCustomConfig) { if (accessStore.useCustomConfig) {
baseUrl = accessStore.moonshotUrl; baseUrl = accessStore.moonshotUrl;
@@ -44,28 +48,28 @@ export class MoonshotApi implements LLMApi {
baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath; baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
} }
if (baseUrl.endsWith("/")) { if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1); baseUrl = baseUrl.slice(0, baseUrl.length - 1);
} }
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Moonshot)) { if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Moonshot)) {
baseUrl = "https://" + baseUrl; 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) { extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? ""; return res.choices?.at(0)?.message?.content ?? '';
} }
speech(options: SpeechOptions): Promise<ArrayBuffer> { speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented."); throw new Error('Method not implemented.');
} }
async chat(options: ChatOptions) { async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = []; const messages: ChatOptions['messages'] = [];
for (const v of options.messages) { for (const v of options.messages) {
const content = getMessageTextContent(v); const content = getMessageTextContent(v);
messages.push({ role: v.role, content }); 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. // 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 shouldStream = !!options.config.stream;
const controller = new AbortController(); const controller = new AbortController();
@@ -101,7 +105,7 @@ export class MoonshotApi implements LLMApi {
try { try {
const chatPath = this.path(Moonshot.ChatPath); const chatPath = this.path(Moonshot.ChatPath);
const chatPayload = { const chatPayload = {
method: "POST", method: 'POST',
body: JSON.stringify(requestPayload), body: JSON.stringify(requestPayload),
signal: controller.signal, signal: controller.signal,
headers: getHeaders(), headers: getHeaders(),
@@ -152,7 +156,7 @@ export class MoonshotApi implements LLMApi {
}); });
} else { } else {
// @ts-ignore // @ts-ignore
runTools[index]["function"]["arguments"] += args; runTools[index].function.arguments += args;
} }
} }
return choices[0]?.delta?.content; return choices[0]?.delta?.content;
@@ -183,10 +187,11 @@ export class MoonshotApi implements LLMApi {
options.onFinish(message, res); options.onFinish(message, res);
} }
} catch (e) { } 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); options.onError?.(e as Error);
} }
} }
async usage() { async usage() {
return { return {
used: 0, used: 0,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -235,12 +235,7 @@
animation: slide-in ease 0.3s; animation: slide-in ease 0.3s;
$linear: linear-gradient( $linear: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1), rgba(0, 0, 0, 0));
to right,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 1),
rgba(0, 0, 0, 0)
);
mask-image: $linear; mask-image: $linear;
@mixin show { @mixin show {
@@ -510,13 +505,8 @@
} }
@media screen and (min-width: 600px) { @media screen and (min-width: 600px) {
$max-image-width: calc( $max-image-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count));
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));
);
$image-width: calc(
calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 /
var(--image-count)
);
.chat-message-item-image-multi { .chat-message-item-image-multi {
width: $image-width; 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, { import EmojiPicker, {
Emoji, Emoji,
EmojiStyle,
Theme as EmojiTheme, Theme as EmojiTheme,
} from "emoji-picker-react"; } from 'emoji-picker-react';
import { ModelType } from "../store"; import BlackBotIcon from '../icons/black-bot.svg';
import BotIcon from '../icons/bot.svg';
import BotIcon from "../icons/bot.svg";
import BlackBotIcon from "../icons/black-bot.svg";
export function getEmojiUrl(unified: string, style: EmojiStyle) { export function getEmojiUrl(unified: string, style: EmojiStyle) {
// Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis // Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis
@@ -21,7 +23,7 @@ export function AvatarPicker(props: {
}) { }) {
return ( return (
<EmojiPicker <EmojiPicker
width={"100%"} width="100%"
lazyLoadEmojis lazyLoadEmojis
theme={EmojiTheme.AUTO} theme={EmojiTheme.AUTO}
getEmojiUrl={getEmojiUrl} getEmojiUrl={getEmojiUrl}
@@ -36,13 +38,15 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
if (props.model) { if (props.model) {
return ( return (
<div className="no-dark"> <div className="no-dark">
{props.model?.startsWith("gpt-4") || {props.model?.startsWith('gpt-4')
props.model?.startsWith("chatgpt-4o") || || props.model?.startsWith('chatgpt-4o')
props.model?.startsWith("o1") ? ( || props.model?.startsWith('o1')
<BlackBotIcon className="user-avatar" /> ? (
) : ( <BlackBotIcon className="user-avatar" />
<BotIcon className="user-avatar" /> )
)} : (
<BotIcon className="user-avatar" />
)}
</div> </div>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,34 +1,35 @@
import { useDebouncedCallback } from "use-debounce"; import type { Plugin } from '../store/plugin';
import OpenAPIClientAxios from "openapi-client-axios"; import clsx from 'clsx';
import yaml from "js-yaml"; import yaml from 'js-yaml';
import { PLUGINS_REPO_URL } from "../constant"; import OpenAPIClientAxios from 'openapi-client-axios';
import { IconButton } from "./button"; import { useState } from 'react';
import { ErrorBoundary } from "./error"; import { useNavigate } from 'react-router-dom';
import styles from "./mask.module.scss"; import { useDebouncedCallback } from 'use-debounce';
import pluginStyles from "./plugin.module.scss"; import { PLUGINS_REPO_URL } from '../constant';
import EditIcon from "../icons/edit.svg"; import AddIcon from '../icons/add.svg';
import AddIcon from "../icons/add.svg"; import CloseIcon from '../icons/close.svg';
import CloseIcon from "../icons/close.svg"; import ConfirmIcon from '../icons/confirm.svg';
import DeleteIcon from "../icons/delete.svg"; import DeleteIcon from '../icons/delete.svg';
import ConfirmIcon from "../icons/confirm.svg"; import EditIcon from '../icons/edit.svg';
import ReloadIcon from "../icons/reload.svg"; import GithubIcon from '../icons/github.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 { import {
PasswordInput,
List, List,
ListItem, ListItem,
Modal, Modal,
PasswordInput,
showConfirm, showConfirm,
showToast, showToast,
} from "./ui-lib"; } from './ui-lib';
import Locale from "../locales";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import clsx from "clsx";
export function PluginPage() { export function PluginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -36,7 +37,7 @@ export function PluginPage() {
const allPlugins = pluginStore.getAll(); const allPlugins = pluginStore.getAll();
const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]); const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState('');
const plugins = searchText.length > 0 ? searchPlugins : allPlugins; const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
// refactored already, now it accurate // refactored already, now it accurate
@@ -44,7 +45,7 @@ export function PluginPage() {
setSearchText(text); setSearchText(text);
if (text.length > 0) { if (text.length > 0) {
const result = allPlugins.filter( const result = allPlugins.filter(
(m) => m?.title.toLowerCase().includes(text.toLowerCase()), m => m?.title.toLowerCase().includes(text.toLowerCase()),
); );
setSearchPlugins(result); setSearchPlugins(result);
} else { } else {
@@ -85,21 +86,21 @@ export function PluginPage() {
} }
}, 100).bind(null, editingPlugin); }, 100).bind(null, editingPlugin);
const [loadUrl, setLoadUrl] = useState<string>(""); const [loadUrl, setLoadUrl] = useState<string>('');
const loadFromUrl = (loadUrl: string) => const loadFromUrl = (loadUrl: string) =>
fetch(loadUrl) fetch(loadUrl)
.catch((e) => { .catch((e) => {
const p = new URL(loadUrl); const p = new URL(loadUrl);
return fetch(`/api/proxy/${p.pathname}?${p.search}`, { return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
headers: { headers: {
"X-Base-URL": p.origin, 'X-Base-URL': p.origin,
}, },
}); });
}) })
.then((res) => res.text()) .then(res => res.text())
.then((content) => { .then((content) => {
try { try {
return JSON.stringify(JSON.parse(content), null, " "); return JSON.stringify(JSON.parse(content), null, ' ');
} catch (e) { } catch (e) {
return content; return content;
} }
@@ -118,7 +119,7 @@ export function PluginPage() {
return ( return (
<ErrorBoundary> <ErrorBoundary>
<div className={styles["mask-page"]}> <div className={styles['mask-page']}>
<div className="window-header"> <div className="window-header">
<div className="window-header-title"> <div className="window-header-title">
<div className="window-header-main-title"> <div className="window-header-main-title">
@@ -149,18 +150,18 @@ export function PluginPage() {
</div> </div>
</div> </div>
<div className={styles["mask-page-body"]}> <div className={styles['mask-page-body']}>
<div className={styles["mask-filter"]}> <div className={styles['mask-filter']}>
<input <input
type="text" type="text"
className={styles["search-bar"]} className={styles['search-bar']}
placeholder={Locale.Plugin.Page.Search} placeholder={Locale.Plugin.Page.Search}
autoFocus autoFocus
onInput={(e) => onSearch(e.currentTarget.value)} onInput={e => onSearch(e.currentTarget.value)}
/> />
<IconButton <IconButton
className={styles["mask-create"]} className={styles['mask-create']}
icon={<AddIcon />} icon={<AddIcon />}
text={Locale.Plugin.Page.Create} text={Locale.Plugin.Page.Create}
bordered bordered
@@ -175,10 +176,10 @@ export function PluginPage() {
{plugins.length == 0 && ( {plugins.length == 0 && (
<div <div
style={{ style={{
display: "flex", display: 'flex',
margin: "60px auto", margin: '60px auto',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
{Locale.Plugin.Page.Find} {Locale.Plugin.Page.Find}
@@ -192,22 +193,24 @@ export function PluginPage() {
</a> </a>
</div> </div>
)} )}
{plugins.map((m) => ( {plugins.map(m => (
<div className={styles["mask-item"]} key={m.id}> <div className={styles['mask-item']} key={m.id}>
<div className={styles["mask-header"]}> <div className={styles['mask-header']}>
<div className={styles["mask-icon"]}></div> <div className={styles['mask-icon']}></div>
<div className={styles["mask-title"]}> <div className={styles['mask-title']}>
<div className={styles["mask-name"]}> <div className={styles['mask-name']}>
{m.title}@<small>{m.version}</small> {m.title}
@
<small>{m.version}</small>
</div> </div>
<div className={clsx(styles["mask-info"], "one-line")}> <div className={clsx(styles['mask-info'], 'one-line')}>
{Locale.Plugin.Item.Info( {Locale.Plugin.Item.Info(
FunctionToolService.add(m).length, FunctionToolService.add(m).length,
)} )}
</div> </div>
</div> </div>
</div> </div>
<div className={styles["mask-actions"]}> <div className={styles['mask-actions']}>
<IconButton <IconButton
icon={<EditIcon />} icon={<EditIcon />}
text={Locale.Plugin.Item.Edit} text={Locale.Plugin.Item.Edit}
@@ -244,7 +247,7 @@ export function PluginPage() {
text={Locale.UI.Confirm} text={Locale.UI.Confirm}
key="export" key="export"
bordered bordered
onClick={() => setEditingPluginId("")} onClick={() => setEditingPluginId('')}
/>, />,
]} ]}
> >
@@ -264,7 +267,7 @@ export function PluginPage() {
<option value="custom">{Locale.Plugin.Auth.Custom}</option> <option value="custom">{Locale.Plugin.Auth.Custom}</option>
</select> </select>
</ListItem> </ListItem>
{["bearer", "basic", "custom"].includes( {['bearer', 'basic', 'custom'].includes(
editingPlugin.authType as string, editingPlugin.authType as string,
) && ( ) && (
<ListItem title={Locale.Plugin.Auth.Location}> <ListItem title={Locale.Plugin.Auth.Location}>
@@ -288,7 +291,7 @@ export function PluginPage() {
</select> </select>
</ListItem> </ListItem>
)} )}
{editingPlugin.authType == "custom" && ( {editingPlugin.authType == 'custom' && (
<ListItem title={Locale.Plugin.Auth.CustomHeader}> <ListItem title={Locale.Plugin.Auth.CustomHeader}>
<input <input
type="text" type="text"
@@ -298,10 +301,11 @@ export function PluginPage() {
plugin.authHeader = e.target.value; plugin.authHeader = e.target.value;
}); });
}} }}
></input> >
</input>
</ListItem> </ListItem>
)} )}
{["bearer", "basic", "custom"].includes( {['bearer', 'basic', 'custom'].includes(
editingPlugin.authType as string, editingPlugin.authType as string,
) && ( ) && (
<ListItem title={Locale.Plugin.Auth.Token}> <ListItem title={Locale.Plugin.Auth.Token}>
@@ -313,18 +317,20 @@ export function PluginPage() {
plugin.authToken = e.currentTarget.value; plugin.authToken = e.currentTarget.value;
}); });
}} }}
></PasswordInput> >
</PasswordInput>
</ListItem> </ListItem>
)} )}
</List> </List>
<List> <List>
<ListItem title={Locale.Plugin.EditModal.Content}> <ListItem title={Locale.Plugin.EditModal.Content}>
<div className={pluginStyles["plugin-schema"]}> <div className={pluginStyles['plugin-schema']}>
<input <input
type="text" type="text"
style={{ minWidth: 200 }} style={{ minWidth: 200 }}
onInput={(e) => setLoadUrl(e.currentTarget.value)} onInput={e => setLoadUrl(e.currentTarget.value)}
></input> >
</input>
<IconButton <IconButton
icon={<ReloadIcon />} icon={<ReloadIcon />}
text={Locale.Plugin.EditModal.Load} text={Locale.Plugin.EditModal.Load}
@@ -334,26 +340,28 @@ export function PluginPage() {
</div> </div>
</ListItem> </ListItem>
<ListItem <ListItem
subTitle={ subTitle={(
<div <div
className={clsx( className={clsx(
"markdown-body", 'markdown-body',
pluginStyles["plugin-content"], pluginStyles['plugin-content'],
)} )}
dir="auto" dir="auto"
> >
<pre> <pre>
<code <code
contentEditable={true} contentEditable
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: editingPlugin.content, __html: editingPlugin.content,
}} }}
onBlur={onChangePlugin} onBlur={onChangePlugin}
></code> >
</code>
</pre> </pre>
</div> </div>
} )}
></ListItem> >
</ListItem>
{editingPluginTool?.tools.map((tool, index) => ( {editingPluginTool?.tools.map((tool, index) => (
<ListItem <ListItem
key={index} 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 type {
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 {
Modality, Modality,
RTClient,
RTInputAudioItem, RTInputAudioItem,
RTResponse, RTResponse,
TurnDetection, TurnDetection,
} from "rt-client"; } from 'rt-client';
import { AudioHandler } from "@/app/lib/audio"; import { IconButton } from '@/app/components/button';
import { uploadImage } from "@/app/utils/chat"; import { VoicePrint } from '@/app/components/voice-print';
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 { interface RealtimeChatProps {
onClose?: () => void; onClose?: () => void;
@@ -36,11 +38,11 @@ export function RealtimeChat({
const chatStore = useChatStore(); const chatStore = useChatStore();
const session = chatStore.currentSession(); const session = chatStore.currentSession();
const config = useAppConfig(); const config = useAppConfig();
const [status, setStatus] = useState(""); const [status, setStatus] = useState('');
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false); const [isConnecting, setIsConnecting] = useState(false);
const [modality, setModality] = useState("audio"); const [modality, setModality] = useState('audio');
const [useVAD, setUseVAD] = useState(true); const [useVAD, setUseVAD] = useState(true);
const [frequencies, setFrequencies] = useState<Uint8Array | undefined>(); const [frequencies, setFrequencies] = useState<Uint8Array | undefined>();
@@ -51,32 +53,33 @@ export function RealtimeChat({
const temperature = config.realtimeConfig.temperature; const temperature = config.realtimeConfig.temperature;
const apiKey = config.realtimeConfig.apiKey; const apiKey = config.realtimeConfig.apiKey;
const model = config.realtimeConfig.model; const model = config.realtimeConfig.model;
const azure = config.realtimeConfig.provider === "Azure"; const azure = config.realtimeConfig.provider === 'Azure';
const azureEndpoint = config.realtimeConfig.azure.endpoint; const azureEndpoint = config.realtimeConfig.azure.endpoint;
const azureDeployment = config.realtimeConfig.azure.deployment; const azureDeployment = config.realtimeConfig.azure.deployment;
const voice = config.realtimeConfig.voice; const voice = config.realtimeConfig.voice;
const handleConnect = async () => { const handleConnect = async () => {
if (isConnecting) return; if (isConnecting)
{ return; }
if (!isConnected) { if (!isConnected) {
try { try {
setIsConnecting(true); setIsConnecting(true);
clientRef.current = azure clientRef.current = azure
? new RTClient( ? new RTClient(
new URL(azureEndpoint), new URL(azureEndpoint),
{ key: apiKey }, { key: apiKey },
{ deployment: azureDeployment }, { deployment: azureDeployment },
) )
: new RTClient({ key: apiKey }, { model }); : new RTClient({ key: apiKey }, { model });
const modalities: Modality[] = const modalities: Modality[]
modality === "audio" ? ["text", "audio"] : ["text"]; = modality === 'audio' ? ['text', 'audio'] : ['text'];
const turnDetection: TurnDetection = useVAD const turnDetection: TurnDetection = useVAD
? { type: "server_vad" } ? { type: 'server_vad' }
: null; : null;
await clientRef.current.configure({ await clientRef.current.configure({
instructions: "", instructions: '',
voice, voice,
input_audio_transcription: { model: "whisper-1" }, input_audio_transcription: { model: 'whisper-1' },
turn_detection: turnDetection, turn_detection: turnDetection,
tools: [], tools: [],
temperature, temperature,
@@ -108,8 +111,8 @@ export function RealtimeChat({
// console.error("Set message failed:", error); // console.error("Set message failed:", error);
// } // }
} catch (error) { } catch (error) {
console.error("Connection failed:", error); console.error('Connection failed:', error);
setStatus("Connection failed"); setStatus('Connection failed');
} finally { } finally {
setIsConnecting(false); setIsConnecting(false);
} }
@@ -125,35 +128,36 @@ export function RealtimeChat({
clientRef.current = null; clientRef.current = null;
setIsConnected(false); setIsConnected(false);
} catch (error) { } catch (error) {
console.error("Disconnect failed:", error); console.error('Disconnect failed:', error);
} }
} }
}; };
const startResponseListener = async () => { const startResponseListener = async () => {
if (!clientRef.current) return; if (!clientRef.current)
{ return; }
try { try {
for await (const serverEvent of clientRef.current.events()) { for await (const serverEvent of clientRef.current.events()) {
if (serverEvent.type === "response") { if (serverEvent.type === 'response') {
await handleResponse(serverEvent); await handleResponse(serverEvent);
} else if (serverEvent.type === "input_audio") { } else if (serverEvent.type === 'input_audio') {
await handleInputAudio(serverEvent); await handleInputAudio(serverEvent);
} }
} }
} catch (error) { } catch (error) {
if (clientRef.current) { if (clientRef.current) {
console.error("Response iteration error:", error); console.error('Response iteration error:', error);
} }
} }
}; };
const handleResponse = async (response: RTResponse) => { const handleResponse = async (response: RTResponse) => {
for await (const item of response) { for await (const item of response) {
if (item.type === "message" && item.role === "assistant") { if (item.type === 'message' && item.role === 'assistant') {
const botMessage = createMessage({ const botMessage = createMessage({
role: item.role, role: item.role,
content: "", content: '',
}); });
// add bot message first // add bot message first
chatStore.updateTargetSession(session, (session) => { chatStore.updateTargetSession(session, (session) => {
@@ -161,11 +165,11 @@ export function RealtimeChat({
}); });
let hasAudio = false; let hasAudio = false;
for await (const content of item) { for await (const content of item) {
if (content.type === "text") { if (content.type === 'text') {
for await (const text of content.textChunks()) { for await (const text of content.textChunks()) {
botMessage.content += text; botMessage.content += text;
} }
} else if (content.type === "audio") { } else if (content.type === 'audio') {
const textTask = async () => { const textTask = async () => {
for await (const text of content.transcriptChunks()) { for await (const text of content.transcriptChunks()) {
botMessage.content += text; botMessage.content += text;
@@ -204,7 +208,7 @@ export function RealtimeChat({
await item.waitForCompletion(); await item.waitForCompletion();
if (item.transcription) { if (item.transcription) {
const userMessage = createMessage({ const userMessage = createMessage({
role: "user", role: 'user',
content: item.transcription, content: item.transcription,
}); });
chatStore.updateTargetSession(session, (session) => { chatStore.updateTargetSession(session, (session) => {
@@ -240,7 +244,7 @@ export function RealtimeChat({
}); });
setIsRecording(true); setIsRecording(true);
} catch (error) { } catch (error) {
console.error("Failed to start recording:", error); console.error('Failed to start recording:', error);
} }
} else if (audioHandlerRef.current) { } else if (audioHandlerRef.current) {
try { try {
@@ -252,14 +256,15 @@ export function RealtimeChat({
} }
setIsRecording(false); setIsRecording(false);
} catch (error) { } catch (error) {
console.error("Failed to stop recording:", error); console.error('Failed to stop recording:', error);
} }
} }
}; };
useEffect(() => { useEffect(() => {
// 防止重复初始化 // 防止重复初始化
if (initRef.current) return; if (initRef.current)
{ return; }
initRef.current = true; initRef.current = true;
const initAudioHandler = async () => { const initAudioHandler = async () => {
@@ -325,16 +330,16 @@ export function RealtimeChat({
}; };
return ( return (
<div className={styles["realtime-chat"]}> <div className={styles['realtime-chat']}>
<div <div
className={clsx(styles["circle-mic"], { className={clsx(styles['circle-mic'], {
[styles["pulse"]]: isRecording, [styles.pulse]: isRecording,
})} })}
> >
<VoicePrint frequencies={frequencies} isActive={isRecording} /> <VoicePrint frequencies={frequencies} isActive={isRecording} />
</div> </div>
<div className={styles["bottom-icons"]}> <div className={styles['bottom-icons']}>
<div> <div>
<IconButton <IconButton
icon={isRecording ? <VoiceIcon /> : <VoiceOffIcon />} icon={isRecording ? <VoiceIcon /> : <VoiceOffIcon />}
@@ -344,7 +349,7 @@ export function RealtimeChat({
bordered bordered
/> />
</div> </div>
<div className={styles["icon-center"]}>{status}</div> <div className={styles['icon-center']}>{status}</div>
<div> <div>
<IconButton <IconButton
icon={<PowerIcon />} 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 type { Voice } from 'rt-client';
import { ListItem, Select, PasswordInput } from "@/app/components/ui-lib"; import { InputRange } from '@/app/components/input-range';
import { InputRange } from "@/app/components/input-range"; import { ListItem, PasswordInput, Select } from '@/app/components/ui-lib';
import { Voice } from "rt-client"; import { ServiceProvider } from '@/app/constant';
import { ServiceProvider } from "@/app/constant"; import Locale from '@/app/locales';
const providers = [ServiceProvider.OpenAI, ServiceProvider.Azure]; 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: { export function RealtimeConfigList(props: {
realtimeConfig: RealtimeConfig; realtimeConfig: RealtimeConfig;
updateConfig: (updater: (config: RealtimeConfig) => void) => void; updateConfig: (updater: (config: RealtimeConfig) => void) => void;
}) { }) {
const azureConfigComponent = props.realtimeConfig.provider === const azureConfigComponent = props.realtimeConfig.provider
ServiceProvider.Azure && ( === ServiceProvider.Azure && (
<> <>
<ListItem <ListItem
title={Locale.Settings.Realtime.Azure.Endpoint.Title} title={Locale.Settings.Realtime.Azure.Endpoint.Title}
@@ -30,7 +30,7 @@ export function RealtimeConfigList(props: {
placeholder={Locale.Settings.Realtime.Azure.Endpoint.Title} placeholder={Locale.Settings.Realtime.Azure.Endpoint.Title}
onChange={(e) => { onChange={(e) => {
props.updateConfig( 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} placeholder={Locale.Settings.Realtime.Azure.Deployment.Title}
onChange={(e) => { onChange={(e) => {
props.updateConfig( 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 <input
type="checkbox" type="checkbox"
checked={props.realtimeConfig.enable} checked={props.realtimeConfig.enable}
onChange={(e) => onChange={e =>
props.updateConfig( props.updateConfig(
(config) => (config.enable = e.currentTarget.checked), config => (config.enable = e.currentTarget.checked),
) )}
} >
></input> </input>
</ListItem> </ListItem>
{props.realtimeConfig.enable && ( {props.realtimeConfig.enable && (
@@ -81,7 +81,7 @@ export function RealtimeConfigList(props: {
value={props.realtimeConfig.provider} value={props.realtimeConfig.provider}
onChange={(e) => { onChange={(e) => {
props.updateConfig( props.updateConfig(
(config) => config =>
(config.provider = e.target.value as ServiceProvider), (config.provider = e.target.value as ServiceProvider),
); );
}} }}
@@ -101,7 +101,7 @@ export function RealtimeConfigList(props: {
aria-label={Locale.Settings.Realtime.Model.Title} aria-label={Locale.Settings.Realtime.Model.Title}
value={props.realtimeConfig.model} value={props.realtimeConfig.model}
onChange={(e) => { onChange={(e) => {
props.updateConfig((config) => (config.model = e.target.value)); props.updateConfig(config => (config.model = e.target.value));
}} }}
> >
{models.map((v, i) => ( {models.map((v, i) => (
@@ -123,7 +123,7 @@ export function RealtimeConfigList(props: {
placeholder={Locale.Settings.Realtime.ApiKey.Placeholder} placeholder={Locale.Settings.Realtime.ApiKey.Placeholder}
onChange={(e) => { onChange={(e) => {
props.updateConfig( 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} value={props.realtimeConfig.voice}
onChange={(e) => { onChange={(e) => {
props.updateConfig( 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" step="0.1"
onChange={(e) => { onChange={(e) => {
props.updateConfig( props.updateConfig(
(config) => config =>
(config.temperature = e.currentTarget.valueAsNumber), (config.temperature = e.currentTarget.valueAsNumber),
); );
}} }}
></InputRange> >
</InputRange>
</ListItem> </ListItem>
</> </>
)} )}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,30 @@
/* eslint-disable @next/next/no-img-element */ import type {
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, {
CSSProperties, CSSProperties,
HTMLProps, HTMLProps,
MouseEvent, MouseEvent,
useEffect, } from 'react';
useState, import clsx from 'clsx';
import React, {
useCallback, useCallback,
useEffect,
useRef, useRef,
} from "react"; useState,
import { IconButton } from "./button"; } from 'react';
import clsx from "clsx"; 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: { export function Popover(props: {
children: JSX.Element; children: JSX.Element;
@@ -35,10 +36,10 @@ export function Popover(props: {
<div className={styles.popover}> <div className={styles.popover}>
{props.children} {props.children}
{props.open && ( {props.open && (
<div className={styles["popover-mask"]} onClick={props.onClose}></div> <div className={styles['popover-mask']} onClick={props.onClose}></div>
)} )}
{props.open && ( {props.open && (
<div className={styles["popover-content"]}>{props.content}</div> <div className={styles['popover-content']}>{props.content}</div>
)} )}
</div> </div>
); );
@@ -62,20 +63,20 @@ export function ListItem(props: {
return ( return (
<div <div
className={clsx( className={clsx(
styles["list-item"], styles['list-item'],
{ {
[styles["vertical"]]: props.vertical, [styles.vertical]: props.vertical,
}, },
props.className, props.className,
)} )}
onClick={props.onClick} onClick={props.onClick}
> >
<div className={styles["list-header"]}> <div className={styles['list-header']}>
{props.icon && <div className={styles["list-icon"]}>{props.icon}</div>} {props.icon && <div className={styles['list-icon']}>{props.icon}</div>}
<div className={styles["list-item-title"]}> <div className={styles['list-item-title']}>
<div>{props.title}</div> <div>{props.title}</div>
{props.subTitle && ( {props.subTitle && (
<div className={styles["list-item-sub-title"]}> <div className={styles['list-item-sub-title']}>
{props.subTitle} {props.subTitle}
</div> </div>
)} )}
@@ -98,11 +99,11 @@ export function Loading() {
return ( return (
<div <div
style={{ style={{
height: "100vh", height: '100vh',
width: "100vw", width: '100vw',
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
<LoadingIcon /> <LoadingIcon />
@@ -121,15 +122,15 @@ interface ModalProps {
export function Modal(props: ModalProps) { export function Modal(props: ModalProps) {
useEffect(() => { useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") { if (e.key === 'Escape') {
props.onClose?.(); props.onClose?.();
} }
}; };
window.addEventListener("keydown", onKeyDown); window.addEventListener('keydown', onKeyDown);
return () => { return () => {
window.removeEventListener("keydown", onKeyDown); window.removeEventListener('keydown', onKeyDown);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@@ -138,22 +139,22 @@ export function Modal(props: ModalProps) {
return ( return (
<div <div
className={clsx(styles["modal-container"], { className={clsx(styles['modal-container'], {
[styles["modal-container-max"]]: isMax, [styles['modal-container-max']]: isMax,
})} })}
> >
<div className={styles["modal-header"]}> <div className={styles['modal-header']}>
<div className={styles["modal-title"]}>{props.title}</div> <div className={styles['modal-title']}>{props.title}</div>
<div className={styles["modal-header-actions"]}> <div className={styles['modal-header-actions']}>
<div <div
className={styles["modal-header-action"]} className={styles['modal-header-action']}
onClick={() => setMax(!isMax)} onClick={() => setMax(!isMax)}
> >
{isMax ? <MinIcon /> : <MaxIcon />} {isMax ? <MinIcon /> : <MaxIcon />}
</div> </div>
<div <div
className={styles["modal-header-action"]} className={styles['modal-header-action']}
onClick={props.onClose} onClick={props.onClose}
> >
<CloseIcon /> <CloseIcon />
@@ -161,13 +162,13 @@ export function Modal(props: ModalProps) {
</div> </div>
</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} {props.footer}
<div className={styles["modal-actions"]}> <div className={styles['modal-actions']}>
{props.actions?.map((action, i) => ( {props.actions?.map((action, i) => (
<div key={i} className={styles["modal-action"]}> <div key={i} className={styles['modal-action']}>
{action} {action}
</div> </div>
))} ))}
@@ -178,8 +179,8 @@ export function Modal(props: ModalProps) {
} }
export function showModal(props: ModalProps) { export function showModal(props: ModalProps) {
const div = document.createElement("div"); const div = document.createElement('div');
div.className = "modal-mask"; div.className = 'modal-mask';
document.body.appendChild(div); document.body.appendChild(div);
const root = createRoot(div); const root = createRoot(div);
@@ -198,19 +199,19 @@ export function showModal(props: ModalProps) {
root.render(<Modal {...props} onClose={closeModal}></Modal>); root.render(<Modal {...props} onClose={closeModal}></Modal>);
} }
export type ToastProps = { export interface ToastProps {
content: string; content: string;
action?: { action?: {
text: string; text: string;
onClick: () => void; onClick: () => void;
}; };
onClose?: () => void; onClose?: () => void;
}; }
export function Toast(props: ToastProps) { export function Toast(props: ToastProps) {
return ( return (
<div className={styles["toast-container"]}> <div className={styles['toast-container']}>
<div className={styles["toast-content"]}> <div className={styles['toast-content']}>
<span>{props.content}</span> <span>{props.content}</span>
{props.action && ( {props.action && (
<button <button
@@ -218,7 +219,7 @@ export function Toast(props: ToastProps) {
props.action?.onClick?.(); props.action?.onClick?.();
props.onClose?.(); props.onClose?.();
}} }}
className={styles["toast-action"]} className={styles['toast-action']}
> >
{props.action.text} {props.action.text}
</button> </button>
@@ -230,10 +231,10 @@ export function Toast(props: ToastProps) {
export function showToast( export function showToast(
content: string, content: string,
action?: ToastProps["action"], action?: ToastProps['action'],
delay = 3000, delay = 3000,
) { ) {
const div = document.createElement("div"); const div = document.createElement('div');
div.className = styles.show; div.className = styles.show;
document.body.appendChild(div); document.body.appendChild(div);
@@ -263,8 +264,9 @@ export function Input(props: InputProps) {
return ( return (
<textarea <textarea
{...props} {...props}
className={clsx(styles["input"], props.className)} className={clsx(styles.input, props.className)}
></textarea> >
</textarea>
); );
} }
@@ -277,17 +279,17 @@ export function PasswordInput(
} }
return ( return (
<div className={"password-input-container"}> <div className="password-input-container">
<IconButton <IconButton
aria={props.aria} aria={props.aria}
icon={visible ? <EyeIcon /> : <EyeOffIcon />} icon={visible ? <EyeIcon /> : <EyeOffIcon />}
onClick={changeVisibility} onClick={changeVisibility}
className={"password-eye"} className="password-eye"
/> />
<input <input
{...props} {...props}
type={visible ? "text" : "password"} type={visible ? 'text' : 'password'}
className={"password-input"} className="password-input"
/> />
</div> </div>
); );
@@ -296,7 +298,7 @@ export function PasswordInput(
export function Select( export function Select(
props: React.DetailedHTMLProps< props: React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement> & { React.SelectHTMLAttributes<HTMLSelectElement> & {
align?: "left" | "center"; align?: 'left' | 'center';
}, },
HTMLSelectElement HTMLSelectElement
>, >,
@@ -305,24 +307,24 @@ export function Select(
return ( return (
<div <div
className={clsx( className={clsx(
styles["select-with-icon"], styles['select-with-icon'],
{ {
[styles["left-align-option"]]: align === "left", [styles['left-align-option']]: align === 'left',
}, },
className, className,
)} )}
> >
<select className={styles["select-with-icon-select"]} {...otherProps}> <select className={styles['select-with-icon-select']} {...otherProps}>
{children} {children}
</select> </select>
<DownIcon className={styles["select-with-icon-icon"]} /> <DownIcon className={styles['select-with-icon-icon']} />
</div> </div>
); );
} }
export function showConfirm(content: any) { export function showConfirm(content: any) {
const div = document.createElement("div"); const div = document.createElement('div');
div.className = "modal-mask"; div.className = 'modal-mask';
document.body.appendChild(div); document.body.appendChild(div);
const root = createRoot(div); const root = createRoot(div);
@@ -347,7 +349,8 @@ export function showConfirm(content: any) {
tabIndex={0} tabIndex={0}
bordered bordered
shadow shadow
></IconButton>, >
</IconButton>,
<IconButton <IconButton
key="confirm" key="confirm"
text={Locale.UI.Confirm} text={Locale.UI.Confirm}
@@ -361,7 +364,8 @@ export function showConfirm(content: any) {
autoFocus autoFocus
bordered bordered
shadow shadow
></IconButton>, >
</IconButton>,
]} ]}
onClose={closeModal} onClose={closeModal}
> >
@@ -384,18 +388,19 @@ function PromptInput(props: {
return ( return (
<textarea <textarea
className={styles["modal-input"]} className={styles['modal-input']}
autoFocus autoFocus
value={input} value={input}
onInput={(e) => onInput(e.currentTarget.value)} onInput={e => onInput(e.currentTarget.value)}
rows={props.rows ?? 3} rows={props.rows ?? 3}
></textarea> >
</textarea>
); );
} }
export function showPrompt(content: any, value = "", rows = 3) { export function showPrompt(content: any, value = '', rows = 3) {
const div = document.createElement("div"); const div = document.createElement('div');
div.className = "modal-mask"; div.className = 'modal-mask';
document.body.appendChild(div); document.body.appendChild(div);
const root = createRoot(div); const root = createRoot(div);
@@ -421,7 +426,8 @@ export function showPrompt(content: any, value = "", rows = 3) {
bordered bordered
shadow shadow
tabIndex={0} tabIndex={0}
></IconButton>, >
</IconButton>,
<IconButton <IconButton
key="confirm" key="confirm"
text={Locale.UI.Confirm} text={Locale.UI.Confirm}
@@ -434,15 +440,17 @@ export function showPrompt(content: any, value = "", rows = 3) {
bordered bordered
shadow shadow
tabIndex={0} tabIndex={0}
></IconButton>, >
</IconButton>,
]} ]}
onClose={closeModal} onClose={closeModal}
> >
<PromptInput <PromptInput
onChange={(val) => (userInput = val)} onChange={val => (userInput = val)}
value={value} value={value}
rows={rows} rows={rows}
></PromptInput> >
</PromptInput>
</Modal>, </Modal>,
); );
}); });
@@ -456,18 +464,19 @@ export function showImageModal(
) { ) {
showModal({ showModal({
title: Locale.Export.Image.Modal, title: Locale.Export.Image.Modal,
defaultMax: defaultMax, defaultMax,
children: ( children: (
<div style={{ display: "flex", justifyContent: "center", ...boxStyle }}> <div style={{ display: 'flex', justifyContent: 'center', ...boxStyle }}>
<img <img
src={img} src={img}
alt="preview" alt="preview"
style={ style={
style ?? { style ?? {
maxWidth: "100%", maxWidth: '100%',
} }
} }
></img> >
</img>
</div> </div>
), ),
}); });
@@ -489,15 +498,15 @@ export function Selector<T>(props: {
Array.isArray(props.defaultSelectedValue) Array.isArray(props.defaultSelectedValue)
? props.defaultSelectedValue ? props.defaultSelectedValue
: props.defaultSelectedValue !== undefined : props.defaultSelectedValue !== undefined
? [props.defaultSelectedValue] ? [props.defaultSelectedValue]
: [], : [],
); );
const handleSelection = (e: MouseEvent, value: T) => { const handleSelection = (e: MouseEvent, value: T) => {
if (props.multiple) { if (props.multiple) {
e.stopPropagation(); e.stopPropagation();
const newSelectedValues = selectedValues.includes(value) const newSelectedValues = selectedValues.includes(value)
? selectedValues.filter((v) => v !== value) ? selectedValues.filter(v => v !== value)
: [...selectedValues, value]; : [...selectedValues, value];
setSelectedValues(newSelectedValues); setSelectedValues(newSelectedValues);
props.onSelection?.(newSelectedValues); props.onSelection?.(newSelectedValues);
@@ -509,15 +518,15 @@ export function Selector<T>(props: {
}; };
return ( return (
<div className={styles["selector"]} onClick={() => props.onClose?.()}> <div className={styles.selector} onClick={() => props.onClose?.()}>
<div className={styles["selector-content"]}> <div className={styles['selector-content']}>
<List> <List>
{props.items.map((item, i) => { {props.items.map((item, i) => {
const selected = selectedValues.includes(item.value); const selected = selectedValues.includes(item.value);
return ( return (
<ListItem <ListItem
className={clsx(styles["selector-item"], { className={clsx(styles['selector-item'], {
[styles["selector-item-disabled"]]: item.disable, [styles['selector-item-disabled']]: item.disable,
})} })}
key={i} key={i}
title={item.title} title={item.title}
@@ -530,18 +539,21 @@ export function Selector<T>(props: {
} }
}} }}
> >
{selected ? ( {selected
<div ? (
style={{ <div
height: 10, style={{
width: 10, height: 10,
backgroundColor: "var(--primary)", width: 10,
borderRadius: 10, backgroundColor: 'var(--primary)',
}} borderRadius: 10,
></div> }}
) : ( >
<></> </div>
)} )
: (
<></>
)}
</ListItem> </ListItem>
); );
})} })}
@@ -567,14 +579,14 @@ export function FullScreen(props: any) {
setFullScreen(!!document.fullscreenElement); setFullScreen(!!document.fullscreenElement);
} }
}; };
document.addEventListener("fullscreenchange", handleScreenChange); document.addEventListener('fullscreenchange', handleScreenChange);
return () => { return () => {
document.removeEventListener("fullscreenchange", handleScreenChange); document.removeEventListener('fullscreenchange', handleScreenChange);
}; };
}, []); }, []);
return ( return (
<div ref={ref} style={{ position: "relative" }} {...rest}> <div ref={ref} style={{ position: 'relative' }} {...rest}>
<div style={{ position: "absolute", right, top }}> <div style={{ position: 'absolute', right, top }}>
<IconButton <IconButton
icon={fullScreen ? <MinIcon /> : <MaxIcon />} icon={fullScreen ? <MinIcon /> : <MaxIcon />}
onClick={toggleFullscreen} 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 { useCallback, useEffect, useRef } from 'react';
import styles from "./voice-print.module.scss"; import styles from './voice-print.module.scss';
interface VoicePrintProps { interface VoicePrintProps {
frequencies?: Uint8Array; frequencies?: Uint8Array;
@@ -29,10 +29,12 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas) return; if (!canvas)
{ return; }
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx)
{ return; }
/** /**
* 处理高DPI屏幕显示 * 处理高DPI屏幕显示
@@ -92,10 +94,10 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
* 3. 根据平均值计算实际显示高度 * 3. 根据平均值计算实际显示高度
*/ */
if (historyRef.current.length > 0) { if (historyRef.current.length > 0) {
const historicalValues = historyRef.current.map((h) => h[i] || 0); const historicalValues = historyRef.current.map(h => h[i] || 0);
avgFrequency = avgFrequency
(avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) / = (avgFrequency + historicalValues.reduce((a, b) => a + b, 0))
(historyRef.current.length + 1); / (historyRef.current.length + 1);
} }
/** /**
@@ -151,9 +153,9 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
* 使用蓝色系配色提升视觉效果 * 使用蓝色系配色提升视觉效果
*/ */
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)"); gradient.addColorStop(0, 'rgba(100, 180, 255, 0.95)');
gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)"); gradient.addColorStop(0.5, 'rgba(140, 200, 255, 0.9)');
gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)"); gradient.addColorStop(1, 'rgba(180, 220, 255, 0.95)');
ctx.fillStyle = gradient; ctx.fillStyle = gradient;
ctx.fill(); ctx.fill();
@@ -173,7 +175,7 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
}, [frequencies, isActive, updateHistory]); }, [frequencies, isActive, updateHistory]);
return ( return (
<div className={styles["voice-print"]}> <div className={styles['voice-print']}>
<canvas ref={canvasRef} /> <canvas ref={canvasRef} />
</div> </div>
); );

View File

@@ -1,20 +1,20 @@
import packageJson from "../../package.json"; import packageJson from '../../package.json';
import { DEFAULT_INPUT_TEMPLATE } from "../constant"; import { DEFAULT_INPUT_TEMPLATE } from '../constant';
export const getBuildConfig = () => { export function getBuildConfig() {
if (typeof process === "undefined") { if (typeof process === 'undefined') {
throw Error( throw new TypeError(
"[Server Config] you are importing a nodejs-only module outside of nodejs", '[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 isApp = !!process.env.BUILD_APP;
const version = "v" + packageJson.version; const version = `v${packageJson.version}`;
const commitInfo = (() => { const commitInfo = (() => {
try { try {
const childProcess = require("child_process"); const childProcess = require('node:child_process');
const commitDate: string = childProcess const commitDate: string = childProcess
.execSync('git log -1 --format="%at000" --date=unix') .execSync('git log -1 --format="%at000" --date=unix')
.toString() .toString()
@@ -26,10 +26,10 @@ export const getBuildConfig = () => {
return { commitDate, commitHash }; return { commitDate, commitHash };
} catch (e) { } 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 { return {
commitDate: "unknown", commitDate: 'unknown',
commitHash: "unknown", commitHash: 'unknown',
}; };
} }
})(); })();
@@ -41,6 +41,6 @@ export const getBuildConfig = () => {
isApp, isApp,
template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE, template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE,
}; };
}; }
export type BuildConfig = ReturnType<typeof getBuildConfig>; 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() { export function getClientConfig() {
if (typeof document !== "undefined") { if (typeof document !== 'undefined') {
// client side // client side
return JSON.parse(queryMeta("config") || "{}") as BuildConfig; return JSON.parse(queryMeta('config') || '{}') as BuildConfig;
} }
if (typeof process !== "undefined") { if (typeof process !== 'undefined') {
// server side // server side
return getBuildConfig(); return getBuildConfig();
} }
@@ -18,9 +19,9 @@ function queryMeta(key: string, defaultValue?: string): string {
const meta = document.head.querySelector( const meta = document.head.querySelector(
`meta[name='${key}']`, `meta[name='${key}']`,
) as HTMLMetaElement; ) as HTMLMetaElement;
ret = meta?.content ?? ""; ret = meta?.content ?? '';
} else { } else {
ret = defaultValue ?? ""; ret = defaultValue ?? '';
} }
return ret; return ret;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
import { SubmitKey } from "../store/config"; import type { PartialLocaleType } from './index';
import type { PartialLocaleType } from "./index"; import { SAAS_CHAT_UTM_URL } from '@/app/constant';
import { getClientConfig } from "../config/client"; import { getClientConfig } from '../config/client';
import { SAAS_CHAT_UTM_URL } from "@/app/constant"; import { SubmitKey } from '../store/config';
const isApp = !!getClientConfig()?.isApp; const isApp = !!getClientConfig()?.isApp;
const de: PartialLocaleType = { const de: PartialLocaleType = {
WIP: "In Bearbeitung...", WIP: 'In Bearbeitung...',
Error: { Error: {
Unauthorized: isApp Unauthorized: isApp
? `😆 Das Gespräch hatte einige Probleme, keine Sorge: ? `😆 Das Gespräch hatte einige Probleme, keine Sorge:
@@ -18,17 +19,17 @@ const de: PartialLocaleType = {
`, `,
}, },
Auth: { Auth: {
Title: "Passwort erforderlich", Title: 'Passwort erforderlich',
Tips: "Der Administrator hat die Passwortüberprüfung aktiviert. Bitte geben Sie den Zugangscode unten ein.", 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.", SubTips: 'Oder geben Sie Ihren OpenAI oder Google API-Schlüssel ein.',
Input: "Geben Sie hier den Zugangscode ein", Input: 'Geben Sie hier den Zugangscode ein',
Confirm: "Bestätigen", Confirm: 'Bestätigen',
Later: "Später", Later: 'Später',
Return: "Zurück", Return: 'Zurück',
SaasTips: SaasTips:
"Die Konfiguration ist zu kompliziert, ich möchte es sofort nutzen", 'Die Konfiguration ist zu kompliziert, ich möchte es sofort nutzen',
TopTips: 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: { ChatItem: {
ChatItemCount: (count: number) => `${count} Gespräche`, ChatItemCount: (count: number) => `${count} Gespräche`,
@@ -36,574 +37,574 @@ const de: PartialLocaleType = {
Chat: { Chat: {
SubTitle: (count: number) => `Insgesamt ${count} Gespräche`, SubTitle: (count: number) => `Insgesamt ${count} Gespräche`,
EditMessage: { EditMessage: {
Title: "Nachricht bearbeiten", Title: 'Nachricht bearbeiten',
Topic: { Topic: {
Title: "Chat-Thema", Title: 'Chat-Thema',
SubTitle: "Ändern Sie das aktuelle Chat-Thema", SubTitle: 'Ändern Sie das aktuelle Chat-Thema',
}, },
}, },
Actions: { Actions: {
ChatList: "Nachrichtliste anzeigen", ChatList: 'Nachrichtliste anzeigen',
CompressedHistory: "Komprimierte Historie anzeigen", CompressedHistory: 'Komprimierte Historie anzeigen',
Export: "Chatverlauf exportieren", Export: 'Chatverlauf exportieren',
Copy: "Kopieren", Copy: 'Kopieren',
Stop: "Stoppen", Stop: 'Stoppen',
Retry: "Erneut versuchen", Retry: 'Erneut versuchen',
Pin: "Anheften", Pin: 'Anheften',
PinToastContent: "1 Gespräch an den voreingestellten Prompt angeheftet", PinToastContent: '1 Gespräch an den voreingestellten Prompt angeheftet',
PinToastAction: "Ansehen", PinToastAction: 'Ansehen',
Delete: "Löschen", Delete: 'Löschen',
Edit: "Bearbeiten", Edit: 'Bearbeiten',
RefreshTitle: "Titel aktualisieren", RefreshTitle: 'Titel aktualisieren',
RefreshToast: "Anfrage zur Titelaktualisierung gesendet", RefreshToast: 'Anfrage zur Titelaktualisierung gesendet',
}, },
Commands: { Commands: {
new: "Neues Gespräch", new: 'Neues Gespräch',
newm: "Neues Gespräch aus Maske erstellen", newm: 'Neues Gespräch aus Maske erstellen',
next: "Nächstes Gespräch", next: 'Nächstes Gespräch',
prev: "Vorheriges Gespräch", prev: 'Vorheriges Gespräch',
clear: "Kontext löschen", clear: 'Kontext löschen',
del: "Gespräch löschen", del: 'Gespräch löschen',
}, },
InputActions: { InputActions: {
Stop: "Antwort stoppen", Stop: 'Antwort stoppen',
ToBottom: "Zum neuesten Beitrag", ToBottom: 'Zum neuesten Beitrag',
Theme: { Theme: {
auto: "Automatisches Thema", auto: 'Automatisches Thema',
light: "Helles Thema", light: 'Helles Thema',
dark: "Dunkles Thema", dark: 'Dunkles Thema',
}, },
Prompt: "Schnellbefehle", Prompt: 'Schnellbefehle',
Masks: "Alle Masken", Masks: 'Alle Masken',
Clear: "Chat löschen", Clear: 'Chat löschen',
Settings: "Gesprächseinstellungen", Settings: 'Gesprächseinstellungen',
UploadImage: "Bild hochladen", UploadImage: 'Bild hochladen',
}, },
Rename: "Gespräch umbenennen", Rename: 'Gespräch umbenennen',
Typing: "Tippt…", Typing: 'Tippt…',
Input: (submitKey: string) => { Input: (submitKey: string) => {
var inputHints = `${submitKey} senden`; let inputHints = `${submitKey} senden`;
if (submitKey === String(SubmitKey.Enter)) { 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: { Config: {
Reset: "Erinnerung löschen", Reset: 'Erinnerung löschen',
SaveAs: "Als Maske speichern", SaveAs: 'Als Maske speichern',
}, },
IsContext: "Voreingestellter Prompt", IsContext: 'Voreingestellter Prompt',
}, },
Export: { Export: {
Title: "Chatverlauf teilen", Title: 'Chatverlauf teilen',
Copy: "Alles kopieren", Copy: 'Alles kopieren',
Download: "Datei herunterladen", Download: 'Datei herunterladen',
Share: "Auf ShareGPT teilen", Share: 'Auf ShareGPT teilen',
MessageFromYou: "Benutzer", MessageFromYou: 'Benutzer',
MessageFromChatGPT: "ChatGPT", MessageFromChatGPT: 'ChatGPT',
Format: { Format: {
Title: "Exportformat", Title: 'Exportformat',
SubTitle: "Kann als Markdown-Text oder PNG-Bild exportiert werden", SubTitle: 'Kann als Markdown-Text oder PNG-Bild exportiert werden',
}, },
IncludeContext: { IncludeContext: {
Title: "Maske Kontext einbeziehen", Title: 'Maske Kontext einbeziehen',
SubTitle: "Soll der Maskenkontext in den Nachrichten angezeigt werden?", SubTitle: 'Soll der Maskenkontext in den Nachrichten angezeigt werden?',
}, },
Steps: { Steps: {
Select: "Auswählen", Select: 'Auswählen',
Preview: "Vorschau", Preview: 'Vorschau',
}, },
Image: { Image: {
Toast: "Screenshot wird erstellt", Toast: 'Screenshot wird erstellt',
Modal: "Lang drücken oder Rechtsklick, um Bild zu speichern", Modal: 'Lang drücken oder Rechtsklick, um Bild zu speichern',
}, },
}, },
Select: { Select: {
Search: "Nachrichten suchen", Search: 'Nachrichten suchen',
All: "Alles auswählen", All: 'Alles auswählen',
Latest: "Neueste", Latest: 'Neueste',
Clear: "Auswahl aufheben", Clear: 'Auswahl aufheben',
}, },
Memory: { Memory: {
Title: "Historische Zusammenfassung", Title: 'Historische Zusammenfassung',
EmptyContent: EmptyContent:
"Gesprächsinhalte sind zu kurz, keine Zusammenfassung erforderlich", 'Gesprächsinhalte sind zu kurz, keine Zusammenfassung erforderlich',
Send: "Chatverlauf automatisch komprimieren und als Kontext senden", Send: 'Chatverlauf automatisch komprimieren und als Kontext senden',
Copy: "Zusammenfassung kopieren", Copy: 'Zusammenfassung kopieren',
Reset: "[nicht verwendet]", Reset: '[nicht verwendet]',
ResetConfirm: "Zusammenfassung löschen bestätigen?", ResetConfirm: 'Zusammenfassung löschen bestätigen?',
}, },
Home: { Home: {
NewChat: "Neues Gespräch", NewChat: 'Neues Gespräch',
DeleteChat: "Bestätigen Sie das Löschen des ausgewählten Gesprächs?", DeleteChat: 'Bestätigen Sie das Löschen des ausgewählten Gesprächs?',
DeleteToast: "Gespräch gelöscht", DeleteToast: 'Gespräch gelöscht',
Revert: "Rückgängig machen", Revert: 'Rückgängig machen',
}, },
Settings: { Settings: {
Title: "Einstellungen", Title: 'Einstellungen',
SubTitle: "Alle Einstellungsmöglichkeiten", SubTitle: 'Alle Einstellungsmöglichkeiten',
Danger: { Danger: {
Reset: { Reset: {
Title: "Alle Einstellungen zurücksetzen", Title: 'Alle Einstellungen zurücksetzen',
SubTitle: "Setzt alle Einstellungen auf die Standardwerte zurück", SubTitle: 'Setzt alle Einstellungen auf die Standardwerte zurück',
Action: "Jetzt zurücksetzen", Action: 'Jetzt zurücksetzen',
Confirm: "Bestätigen Sie das Zurücksetzen aller Einstellungen?", Confirm: 'Bestätigen Sie das Zurücksetzen aller Einstellungen?',
}, },
Clear: { Clear: {
Title: "Alle Daten löschen", Title: 'Alle Daten löschen',
SubTitle: "Löscht alle Chats und Einstellungsdaten", SubTitle: 'Löscht alle Chats und Einstellungsdaten',
Action: "Jetzt löschen", Action: 'Jetzt löschen',
Confirm: Confirm:
"Bestätigen Sie das Löschen aller Chats und Einstellungsdaten?", 'Bestätigen Sie das Löschen aller Chats und Einstellungsdaten?',
}, },
}, },
Lang: { Lang: {
Name: "Sprache", // ACHTUNG: Wenn Sie eine neue Übersetzung hinzufügen möchten, übersetzen Sie diesen Wert bitte nicht, lassen Sie ihn als `Sprache` Name: 'Sprache', // ACHTUNG: Wenn Sie eine neue Übersetzung hinzufügen möchten, übersetzen Sie diesen Wert bitte nicht, lassen Sie ihn als `Sprache`
All: "Alle Sprachen", All: 'Alle Sprachen',
}, },
Avatar: "Avatar", Avatar: 'Avatar',
FontSize: { FontSize: {
Title: "Schriftgröße", Title: 'Schriftgröße',
SubTitle: "Schriftgröße des Chat-Inhalts", SubTitle: 'Schriftgröße des Chat-Inhalts',
}, },
FontFamily: { FontFamily: {
Title: "Chat-Schriftart", Title: 'Chat-Schriftart',
SubTitle: SubTitle:
"Schriftart des Chat-Inhalts, leer lassen, um die globale Standardschriftart anzuwenden", 'Schriftart des Chat-Inhalts, leer lassen, um die globale Standardschriftart anzuwenden',
Placeholder: "Schriftartname", Placeholder: 'Schriftartname',
}, },
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "Systemweite Eingabeaufforderungen einfügen", Title: 'Systemweite Eingabeaufforderungen einfügen',
SubTitle: 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: { InputTemplate: {
Title: "Benutzer-Eingabeverarbeitung", Title: 'Benutzer-Eingabeverarbeitung',
SubTitle: SubTitle:
"Die neueste Nachricht des Benutzers wird in diese Vorlage eingefügt", 'Die neueste Nachricht des Benutzers wird in diese Vorlage eingefügt',
}, },
Update: { Update: {
Version: (x: string) => `Aktuelle Version: ${x}`, Version: (x: string) => `Aktuelle Version: ${x}`,
IsLatest: "Bereits die neueste Version", IsLatest: 'Bereits die neueste Version',
CheckUpdate: "Auf Updates überprüfen", CheckUpdate: 'Auf Updates überprüfen',
IsChecking: "Überprüfe auf Updates...", IsChecking: 'Überprüfe auf Updates...',
FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`, FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`,
GoToUpdate: "Zum Update gehen", GoToUpdate: 'Zum Update gehen',
}, },
SendKey: "Sende-Taste", SendKey: 'Sende-Taste',
Theme: "Thema", Theme: 'Thema',
TightBorder: "Randloser Modus", TightBorder: 'Randloser Modus',
SendPreviewBubble: { SendPreviewBubble: {
Title: "Vorschau-Bubble", Title: 'Vorschau-Bubble',
SubTitle: "Markdown-Inhalt in der Vorschau-Bubble anzeigen", SubTitle: 'Markdown-Inhalt in der Vorschau-Bubble anzeigen',
}, },
AutoGenerateTitle: { AutoGenerateTitle: {
Title: "Titel automatisch generieren", Title: 'Titel automatisch generieren',
SubTitle: SubTitle:
"Basierend auf dem Chat-Inhalt einen passenden Titel generieren", 'Basierend auf dem Chat-Inhalt einen passenden Titel generieren',
}, },
Sync: { Sync: {
CloudState: "Cloud-Daten", CloudState: 'Cloud-Daten',
NotSyncYet: "Noch nicht synchronisiert", NotSyncYet: 'Noch nicht synchronisiert',
Success: "Synchronisation erfolgreich", Success: 'Synchronisation erfolgreich',
Fail: "Synchronisation fehlgeschlagen", Fail: 'Synchronisation fehlgeschlagen',
Config: { Config: {
Modal: { Modal: {
Title: "Cloud-Synchronisation konfigurieren", Title: 'Cloud-Synchronisation konfigurieren',
Check: "Verfügbarkeit überprüfen", Check: 'Verfügbarkeit überprüfen',
}, },
SyncType: { SyncType: {
Title: "Synchronisationstyp", Title: 'Synchronisationstyp',
SubTitle: "Wählen Sie den bevorzugten Synchronisationsserver aus", SubTitle: 'Wählen Sie den bevorzugten Synchronisationsserver aus',
}, },
Proxy: { Proxy: {
Title: "Proxy aktivieren", Title: 'Proxy aktivieren',
SubTitle: 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: { ProxyUrl: {
Title: "Proxy-Adresse", Title: 'Proxy-Adresse',
SubTitle: "Nur für projektinterne Cross-Origin-Proxy", SubTitle: 'Nur für projektinterne Cross-Origin-Proxy',
}, },
WebDav: { WebDav: {
Endpoint: "WebDAV-Adresse", Endpoint: 'WebDAV-Adresse',
UserName: "Benutzername", UserName: 'Benutzername',
Password: "Passwort", Password: 'Passwort',
}, },
UpStash: { UpStash: {
Endpoint: "UpStash Redis REST-Url", Endpoint: 'UpStash Redis REST-Url',
UserName: "Sicherungsname", UserName: 'Sicherungsname',
Password: "UpStash Redis REST-Token", Password: 'UpStash Redis REST-Token',
}, },
}, },
LocalState: "Lokale Daten", LocalState: 'Lokale Daten',
Overview: (overview: any) => { Overview: (overview: any) => {
return `${overview.chat} Chats, ${overview.message} Nachrichten, ${overview.prompt} Eingabeaufforderungen, ${overview.mask} Masken`; return `${overview.chat} Chats, ${overview.message} Nachrichten, ${overview.prompt} Eingabeaufforderungen, ${overview.mask} Masken`;
}, },
ImportFailed: "Import fehlgeschlagen", ImportFailed: 'Import fehlgeschlagen',
}, },
Mask: { Mask: {
Splash: { Splash: {
Title: "Masken-Startseite", Title: 'Masken-Startseite',
SubTitle: SubTitle:
"Zeige die Masken-Startseite beim Erstellen eines neuen Chats", 'Zeige die Masken-Startseite beim Erstellen eines neuen Chats',
}, },
Builtin: { Builtin: {
Title: "Eingebaute Masken ausblenden", Title: 'Eingebaute Masken ausblenden',
SubTitle: "Blendet eingebaute Masken in allen Maskenlisten aus", SubTitle: 'Blendet eingebaute Masken in allen Maskenlisten aus',
}, },
}, },
Prompt: { Prompt: {
Disable: { Disable: {
Title: "Automatische Eingabeaufforderung deaktivieren", Title: 'Automatische Eingabeaufforderung deaktivieren',
SubTitle: 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) => ListCount: (builtin: number, custom: number) =>
`Eingebaut ${builtin} Stück, Benutzerdefiniert ${custom} Stück`, `Eingebaut ${builtin} Stück, Benutzerdefiniert ${custom} Stück`,
Edit: "Bearbeiten", Edit: 'Bearbeiten',
Modal: { Modal: {
Title: "Eingabeaufforderungsliste", Title: 'Eingabeaufforderungsliste',
Add: "Neu erstellen", Add: 'Neu erstellen',
Search: "Eingabeaufforderungen suchen", Search: 'Eingabeaufforderungen suchen',
}, },
EditModal: { EditModal: {
Title: "Eingabeaufforderung bearbeiten", Title: 'Eingabeaufforderung bearbeiten',
}, },
}, },
HistoryCount: { HistoryCount: {
Title: "Anzahl der historischen Nachrichten", Title: 'Anzahl der historischen Nachrichten',
SubTitle: SubTitle:
"Anzahl der historischen Nachrichten, die bei jeder Anfrage mitgesendet werden", 'Anzahl der historischen Nachrichten, die bei jeder Anfrage mitgesendet werden',
}, },
CompressThreshold: { CompressThreshold: {
Title: "Komprimierungsschwelle für historische Nachrichtenlänge", Title: 'Komprimierungsschwelle für historische Nachrichtenlänge',
SubTitle: SubTitle:
"Wenn die unkomprimierten historischen Nachrichten diesen Wert überschreiten, wird komprimiert", 'Wenn die unkomprimierten historischen Nachrichten diesen Wert überschreiten, wird komprimiert',
}, },
Usage: { Usage: {
Title: "Guthabenabfrage", Title: 'Guthabenabfrage',
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
return `In diesem Monat verwendet $${used}, Abonnement insgesamt $${total}`; return `In diesem Monat verwendet $${used}, Abonnement insgesamt $${total}`;
}, },
IsChecking: "Wird überprüft…", IsChecking: 'Wird überprüft…',
Check: "Erneut überprüfen", Check: 'Erneut überprüfen',
NoAccess: 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: { Access: {
SaasStart: { SaasStart: {
Title: "NextChat AI verwenden", Title: 'NextChat AI verwenden',
Label: "(Die kosteneffektivste Lösung)", Label: '(Die kosteneffektivste Lösung)',
SubTitle: 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", '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", ChatNow: 'Jetzt chatten',
}, },
AccessCode: { AccessCode: {
Title: "Zugangscode", Title: 'Zugangscode',
SubTitle: SubTitle:
"Der Administrator hat die verschlüsselte Zugriffskontrolle aktiviert", 'Der Administrator hat die verschlüsselte Zugriffskontrolle aktiviert',
Placeholder: "Geben Sie den Zugangscode ein", Placeholder: 'Geben Sie den Zugangscode ein',
}, },
CustomEndpoint: { CustomEndpoint: {
Title: "Benutzerdefinierte Schnittstelle", Title: 'Benutzerdefinierte Schnittstelle',
SubTitle: "Benutzerdefinierte Azure- oder OpenAI-Dienste verwenden", SubTitle: 'Benutzerdefinierte Azure- oder OpenAI-Dienste verwenden',
}, },
Provider: { Provider: {
Title: "Modellanbieter", Title: 'Modellanbieter',
SubTitle: "Wechseln Sie zu verschiedenen Anbietern", SubTitle: 'Wechseln Sie zu verschiedenen Anbietern',
}, },
OpenAI: { OpenAI: {
ApiKey: { ApiKey: {
Title: "API-Schlüssel", Title: 'API-Schlüssel',
SubTitle: SubTitle:
"Verwenden Sie benutzerdefinierten OpenAI-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen", 'Verwenden Sie benutzerdefinierten OpenAI-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen',
Placeholder: "OpenAI API-Schlüssel", Placeholder: 'OpenAI API-Schlüssel',
}, },
Endpoint: { Endpoint: {
Title: "Schnittstellenadresse", Title: 'Schnittstellenadresse',
SubTitle: "Neben der Standardadresse muss http(s):// enthalten sein", SubTitle: 'Neben der Standardadresse muss http(s):// enthalten sein',
}, },
}, },
Azure: { Azure: {
ApiKey: { ApiKey: {
Title: "Schnittstellenschlüssel", Title: 'Schnittstellenschlüssel',
SubTitle: SubTitle:
"Verwenden Sie benutzerdefinierten Azure-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen", 'Verwenden Sie benutzerdefinierten Azure-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen',
Placeholder: "Azure API-Schlüssel", Placeholder: 'Azure API-Schlüssel',
}, },
Endpoint: { Endpoint: {
Title: "Schnittstellenadresse", Title: 'Schnittstellenadresse',
SubTitle: "Beispiel:", SubTitle: 'Beispiel:',
}, },
ApiVerion: { ApiVerion: {
Title: "Schnittstellenversion (azure api version)", Title: 'Schnittstellenversion (azure api version)',
SubTitle: "Wählen Sie eine spezifische Teilversion aus", SubTitle: 'Wählen Sie eine spezifische Teilversion aus',
}, },
}, },
Anthropic: { Anthropic: {
ApiKey: { ApiKey: {
Title: "Schnittstellenschlüssel", Title: 'Schnittstellenschlüssel',
SubTitle: SubTitle:
"Verwenden Sie benutzerdefinierten Anthropic-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen", 'Verwenden Sie benutzerdefinierten Anthropic-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen',
Placeholder: "Anthropic API-Schlüssel", Placeholder: 'Anthropic API-Schlüssel',
}, },
Endpoint: { Endpoint: {
Title: "Schnittstellenadresse", Title: 'Schnittstellenadresse',
SubTitle: "Beispiel:", SubTitle: 'Beispiel:',
}, },
ApiVerion: { ApiVerion: {
Title: "Schnittstellenversion (claude api version)", Title: 'Schnittstellenversion (claude api version)',
SubTitle: "Wählen Sie eine spezifische API-Version aus", SubTitle: 'Wählen Sie eine spezifische API-Version aus',
}, },
}, },
Google: { Google: {
ApiKey: { ApiKey: {
Title: "API-Schlüssel", Title: 'API-Schlüssel',
SubTitle: "Holen Sie sich Ihren API-Schlüssel von Google AI", SubTitle: 'Holen Sie sich Ihren API-Schlüssel von Google AI',
Placeholder: "Geben Sie Ihren Google AI Studio API-Schlüssel ein", Placeholder: 'Geben Sie Ihren Google AI Studio API-Schlüssel ein',
}, },
Endpoint: { Endpoint: {
Title: "Endpunktadresse", Title: 'Endpunktadresse',
SubTitle: "Beispiel:", SubTitle: 'Beispiel:',
}, },
ApiVersion: { ApiVersion: {
Title: "API-Version (nur für gemini-pro)", Title: 'API-Version (nur für gemini-pro)',
SubTitle: "Wählen Sie eine spezifische API-Version aus", SubTitle: 'Wählen Sie eine spezifische API-Version aus',
}, },
GoogleSafetySettings: { GoogleSafetySettings: {
Title: "Google Sicherheitsfilterstufe", Title: 'Google Sicherheitsfilterstufe',
SubTitle: "Inhaltfilterstufe einstellen", SubTitle: 'Inhaltfilterstufe einstellen',
}, },
}, },
Baidu: { Baidu: {
ApiKey: { ApiKey: {
Title: "API-Schlüssel", Title: 'API-Schlüssel',
SubTitle: "Verwenden Sie benutzerdefinierten Baidu API-Schlüssel", SubTitle: 'Verwenden Sie benutzerdefinierten Baidu API-Schlüssel',
Placeholder: "Baidu API-Schlüssel", Placeholder: 'Baidu API-Schlüssel',
}, },
SecretKey: { SecretKey: {
Title: "Geheimschlüssel", Title: 'Geheimschlüssel',
SubTitle: "Verwenden Sie benutzerdefinierten Baidu Geheimschlüssel", SubTitle: 'Verwenden Sie benutzerdefinierten Baidu Geheimschlüssel',
Placeholder: "Baidu Geheimschlüssel", Placeholder: 'Baidu Geheimschlüssel',
}, },
Endpoint: { Endpoint: {
Title: "Schnittstellenadresse", Title: 'Schnittstellenadresse',
SubTitle: SubTitle:
"Keine benutzerdefinierten Adressen unterstützen, konfigurieren Sie in .env", 'Keine benutzerdefinierten Adressen unterstützen, konfigurieren Sie in .env',
}, },
}, },
ByteDance: { ByteDance: {
ApiKey: { ApiKey: {
Title: "Schnittstellenschlüssel", Title: 'Schnittstellenschlüssel',
SubTitle: "Verwenden Sie benutzerdefinierten ByteDance API-Schlüssel", SubTitle: 'Verwenden Sie benutzerdefinierten ByteDance API-Schlüssel',
Placeholder: "ByteDance API-Schlüssel", Placeholder: 'ByteDance API-Schlüssel',
}, },
Endpoint: { Endpoint: {
Title: "Schnittstellenadresse", Title: 'Schnittstellenadresse',
SubTitle: "Beispiel:", SubTitle: 'Beispiel:',
}, },
}, },
Alibaba: { Alibaba: {
ApiKey: { ApiKey: {
Title: "Schnittstellenschlüssel", Title: 'Schnittstellenschlüssel',
SubTitle: SubTitle:
"Verwenden Sie benutzerdefinierten Alibaba Cloud API-Schlüssel", 'Verwenden Sie benutzerdefinierten Alibaba Cloud API-Schlüssel',
Placeholder: "Alibaba Cloud API-Schlüssel", Placeholder: 'Alibaba Cloud API-Schlüssel',
}, },
Endpoint: { Endpoint: {
Title: "Schnittstellenadresse", Title: 'Schnittstellenadresse',
SubTitle: "Beispiel:", SubTitle: 'Beispiel:',
}, },
}, },
CustomModel: { CustomModel: {
Title: "Benutzerdefinierter Modellname", Title: 'Benutzerdefinierter Modellname',
SubTitle: SubTitle:
"Fügen Sie benutzerdefinierte Modelloptionen hinzu, getrennt durch Kommas", 'Fügen Sie benutzerdefinierte Modelloptionen hinzu, getrennt durch Kommas',
}, },
}, },
Model: "Modell", Model: 'Modell',
CompressModel: { CompressModel: {
Title: "Kompressionsmodell", Title: 'Kompressionsmodell',
SubTitle: "Modell zur Komprimierung des Verlaufs", SubTitle: 'Modell zur Komprimierung des Verlaufs',
}, },
Temperature: { Temperature: {
Title: "Zufälligkeit (temperature)", Title: 'Zufälligkeit (temperature)',
SubTitle: "Je höher der Wert, desto zufälliger die Antwort", SubTitle: 'Je höher der Wert, desto zufälliger die Antwort',
}, },
TopP: { TopP: {
Title: "Kern-Sampling (top_p)", Title: 'Kern-Sampling (top_p)',
SubTitle: SubTitle:
"Ähnlich der Zufälligkeit, aber nicht zusammen mit Zufälligkeit ändern", 'Ähnlich der Zufälligkeit, aber nicht zusammen mit Zufälligkeit ändern',
}, },
MaxTokens: { MaxTokens: {
Title: "Maximale Token-Anzahl pro Antwort", Title: 'Maximale Token-Anzahl pro Antwort',
SubTitle: "Maximale Anzahl der Tokens pro Interaktion", SubTitle: 'Maximale Anzahl der Tokens pro Interaktion',
}, },
PresencePenalty: { PresencePenalty: {
Title: "Themenfrische (presence_penalty)", Title: 'Themenfrische (presence_penalty)',
SubTitle: 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: { FrequencyPenalty: {
Title: "Häufigkeitsstrafe (frequency_penalty)", Title: 'Häufigkeitsstrafe (frequency_penalty)',
SubTitle: 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: { Store: {
DefaultTopic: "Neuer Chat", DefaultTopic: 'Neuer Chat',
BotHello: "Wie kann ich Ihnen helfen?", BotHello: 'Wie kann ich Ihnen helfen?',
Error: 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: { Prompt: {
History: (content: string) => History: (content: string) =>
"Dies ist eine Zusammenfassung des bisherigen Chats als Hintergrundinformation: " + `Dies ist eine Zusammenfassung des bisherigen Chats als Hintergrundinformation: ${
content, content}`,
Topic: 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: 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: { Copy: {
Success: "In die Zwischenablage geschrieben", Success: 'In die Zwischenablage geschrieben',
Failed: Failed:
"Kopieren fehlgeschlagen, bitte erlauben Sie Zugriff auf die Zwischenablage", 'Kopieren fehlgeschlagen, bitte erlauben Sie Zugriff auf die Zwischenablage',
}, },
Download: { Download: {
Success: "Inhalt wurde in Ihrem Verzeichnis heruntergeladen.", Success: 'Inhalt wurde in Ihrem Verzeichnis heruntergeladen.',
Failed: "Download fehlgeschlagen.", Failed: 'Download fehlgeschlagen.',
}, },
Context: { Context: {
Toast: (x: any) => `Beinhaltet ${x} vordefinierte Eingabeaufforderungen`, Toast: (x: any) => `Beinhaltet ${x} vordefinierte Eingabeaufforderungen`,
Edit: "Aktuelle Gesprächseinstellungen", Edit: 'Aktuelle Gesprächseinstellungen',
Add: "Neues Gespräch hinzufügen", Add: 'Neues Gespräch hinzufügen',
Clear: "Kontext gelöscht", Clear: 'Kontext gelöscht',
Revert: "Kontext wiederherstellen", Revert: 'Kontext wiederherstellen',
}, },
Plugin: { Plugin: {
Name: "Plugins", Name: 'Plugins',
}, },
FineTuned: { FineTuned: {
Sysmessage: "Du bist ein Assistent", Sysmessage: 'Du bist ein Assistent',
}, },
SearchChat: { SearchChat: {
Name: "Suche", Name: 'Suche',
Page: { Page: {
Title: "Chatverlauf durchsuchen", Title: 'Chatverlauf durchsuchen',
Search: "Suchbegriff eingeben", Search: 'Suchbegriff eingeben',
NoResult: "Keine Ergebnisse gefunden", NoResult: 'Keine Ergebnisse gefunden',
NoData: "Keine Daten", NoData: 'Keine Daten',
Loading: "Laden", Loading: 'Laden',
SubTitle: (count: number) => `${count} Ergebnisse gefunden`, SubTitle: (count: number) => `${count} Ergebnisse gefunden`,
}, },
Item: { Item: {
View: "Ansehen", View: 'Ansehen',
}, },
}, },
Mask: { Mask: {
Name: "Masken", Name: 'Masken',
Page: { Page: {
Title: "Vordefinierte Rollenmasken", Title: 'Vordefinierte Rollenmasken',
SubTitle: (count: number) => SubTitle: (count: number) =>
`${count} vordefinierte Rollenbeschreibungen`, `${count} vordefinierte Rollenbeschreibungen`,
Search: "Rollenmasken suchen", Search: 'Rollenmasken suchen',
Create: "Neu erstellen", Create: 'Neu erstellen',
}, },
Item: { Item: {
Info: (count: number) => `Beinhaltet ${count} vordefinierte Gespräche`, Info: (count: number) => `Beinhaltet ${count} vordefinierte Gespräche`,
Chat: "Gespräch", Chat: 'Gespräch',
View: "Anzeigen", View: 'Anzeigen',
Edit: "Bearbeiten", Edit: 'Bearbeiten',
Delete: "Löschen", Delete: 'Löschen',
DeleteConfirm: "Bestätigen Sie das Löschen?", DeleteConfirm: 'Bestätigen Sie das Löschen?',
}, },
EditModal: { EditModal: {
Title: (readonly: boolean) => Title: (readonly: boolean) =>
`Vordefinierte Maske bearbeiten ${readonly ? "Nur lesen" : ""}`, `Vordefinierte Maske bearbeiten ${readonly ? 'Nur lesen' : ''}`,
Download: "Vorgabe herunterladen", Download: 'Vorgabe herunterladen',
Clone: "Vorgabe klonen", Clone: 'Vorgabe klonen',
}, },
Config: { Config: {
Avatar: "Rollen-Avatar", Avatar: 'Rollen-Avatar',
Name: "Rollenname", Name: 'Rollenname',
Sync: { Sync: {
Title: "Globale Einstellungen verwenden", Title: 'Globale Einstellungen verwenden',
SubTitle: SubTitle:
"Soll das aktuelle Gespräch die globalen Modelleinstellungen verwenden?", 'Soll das aktuelle Gespräch die globalen Modelleinstellungen verwenden?',
Confirm: 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: { HideContext: {
Title: "Vordefinierte Gespräche ausblenden", Title: 'Vordefinierte Gespräche ausblenden',
SubTitle: 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: { Share: {
Title: "Diese Maske teilen", Title: 'Diese Maske teilen',
SubTitle: "Generieren Sie einen Direktlink zu dieser Maske", SubTitle: 'Generieren Sie einen Direktlink zu dieser Maske',
Action: "Link kopieren", Action: 'Link kopieren',
}, },
}, },
}, },
NewChat: { NewChat: {
Return: "Zurück", Return: 'Zurück',
Skip: "Direkt beginnen", Skip: 'Direkt beginnen',
NotShow: "Nicht mehr anzeigen", NotShow: 'Nicht mehr anzeigen',
ConfirmNoShow: ConfirmNoShow:
"Bestätigen Sie die Deaktivierung? Nach der Deaktivierung können Sie jederzeit in den Einstellungen wieder aktivieren.", 'Bestätigen Sie die Deaktivierung? Nach der Deaktivierung können Sie jederzeit in den Einstellungen wieder aktivieren.',
Title: "Wählen Sie eine Maske aus", Title: 'Wählen Sie eine Maske aus',
SubTitle: SubTitle:
"Starten Sie jetzt und lassen Sie sich von den Gedanken hinter der Maske inspirieren", 'Starten Sie jetzt und lassen Sie sich von den Gedanken hinter der Maske inspirieren',
More: "Alle anzeigen", More: 'Alle anzeigen',
}, },
URLCommand: { 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: 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: { UI: {
Confirm: "Bestätigen", Confirm: 'Bestätigen',
Cancel: "Abbrechen", Cancel: 'Abbrechen',
Close: "Schließen", Close: 'Schließen',
Create: "Neu erstellen", Create: 'Neu erstellen',
Edit: "Bearbeiten", Edit: 'Bearbeiten',
Export: "Exportieren", Export: 'Exportieren',
Import: "Importieren", Import: 'Importieren',
Sync: "Synchronisieren", Sync: 'Synchronisieren',
Config: "Konfigurieren", Config: 'Konfigurieren',
}, },
Exporter: { Exporter: {
Description: { 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", Model: 'Modell',
Messages: "Nachrichten", Messages: 'Nachrichten',
Topic: "Thema", Topic: 'Thema',
Time: "Zeit", 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 type { PartialLocaleType } from "./index"; import { SAAS_CHAT_UTM_URL } from '@/app/constant';
import { getClientConfig } from "../config/client"; import { getClientConfig } from '../config/client';
import { SAAS_CHAT_UTM_URL } from "@/app/constant"; import { SubmitKey } from '../store/config';
const isApp = !!getClientConfig()?.isApp; const isApp = !!getClientConfig()?.isApp;
const es: PartialLocaleType = { const es: PartialLocaleType = {
WIP: "En construcción...", WIP: 'En construcción...',
Error: { Error: {
Unauthorized: isApp Unauthorized: isApp
? `😆 La conversación encontró algunos problemas, no te preocupes: ? `😆 La conversación encontró algunos problemas, no te preocupes:
@@ -18,17 +19,17 @@ const es: PartialLocaleType = {
`, `,
}, },
Auth: { Auth: {
Title: "Se requiere contraseña", 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", 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", SubTips: 'O ingresa tu clave API de OpenAI o Google',
Input: "Introduce el código de acceso aquí", Input: 'Introduce el código de acceso aquí',
Confirm: "Confirmar", Confirm: 'Confirmar',
Later: "Más tarde", Later: 'Más tarde',
Return: "Regresar", Return: 'Regresar',
SaasTips: SaasTips:
"La configuración es demasiado complicada, quiero usarlo de inmediato", 'La configuración es demasiado complicada, quiero usarlo de inmediato',
TopTips: 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: { ChatItem: {
ChatItemCount: (count: number) => `${count} conversaciones`, ChatItemCount: (count: number) => `${count} conversaciones`,
@@ -36,570 +37,570 @@ const es: PartialLocaleType = {
Chat: { Chat: {
SubTitle: (count: number) => `Total de ${count} conversaciones`, SubTitle: (count: number) => `Total de ${count} conversaciones`,
EditMessage: { EditMessage: {
Title: "Editar registro de mensajes", Title: 'Editar registro de mensajes',
Topic: { Topic: {
Title: "Tema de la conversación", Title: 'Tema de la conversación',
SubTitle: "Cambiar el tema de la conversación actual", SubTitle: 'Cambiar el tema de la conversación actual',
}, },
}, },
Actions: { Actions: {
ChatList: "Ver lista de mensajes", ChatList: 'Ver lista de mensajes',
CompressedHistory: "Ver historial de Prompts comprimidos", CompressedHistory: 'Ver historial de Prompts comprimidos',
Export: "Exportar historial de chat", Export: 'Exportar historial de chat',
Copy: "Copiar", Copy: 'Copiar',
Stop: "Detener", Stop: 'Detener',
Retry: "Reintentar", Retry: 'Reintentar',
Pin: "Fijar", Pin: 'Fijar',
PinToastContent: PinToastContent:
"Se ha fijado 1 conversación a los prompts predeterminados", 'Se ha fijado 1 conversación a los prompts predeterminados',
PinToastAction: "Ver", PinToastAction: 'Ver',
Delete: "Eliminar", Delete: 'Eliminar',
Edit: "Editar", Edit: 'Editar',
RefreshTitle: "Actualizar título", RefreshTitle: 'Actualizar título',
RefreshToast: "Se ha enviado la solicitud de actualización del título", RefreshToast: 'Se ha enviado la solicitud de actualización del título',
}, },
Commands: { Commands: {
new: "Nueva conversación", new: 'Nueva conversación',
newm: "Nueva conversación desde la máscara", newm: 'Nueva conversación desde la máscara',
next: "Siguiente conversación", next: 'Siguiente conversación',
prev: "Conversación anterior", prev: 'Conversación anterior',
clear: "Limpiar contexto", clear: 'Limpiar contexto',
del: "Eliminar conversación", del: 'Eliminar conversación',
}, },
InputActions: { InputActions: {
Stop: "Detener respuesta", Stop: 'Detener respuesta',
ToBottom: "Ir al más reciente", ToBottom: 'Ir al más reciente',
Theme: { Theme: {
auto: "Tema automático", auto: 'Tema automático',
light: "Modo claro", light: 'Modo claro',
dark: "Modo oscuro", dark: 'Modo oscuro',
}, },
Prompt: "Comandos rápidos", Prompt: 'Comandos rápidos',
Masks: "Todas las máscaras", Masks: 'Todas las máscaras',
Clear: "Limpiar chat", Clear: 'Limpiar chat',
Settings: "Configuración de conversación", Settings: 'Configuración de conversación',
UploadImage: "Subir imagen", UploadImage: 'Subir imagen',
}, },
Rename: "Renombrar conversación", Rename: 'Renombrar conversación',
Typing: "Escribiendo…", Typing: 'Escribiendo…',
Input: (submitKey: string) => { Input: (submitKey: string) => {
var inputHints = `${submitKey} para enviar`; let inputHints = `${submitKey} para enviar`;
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter para nueva línea"; inputHints += 'Shift + Enter para nueva línea';
} }
return ( return (
inputHints + "/ para activar autocompletado: para activar comandos" `${inputHints}/ para activar autocompletado: para activar comandos`
); );
}, },
Send: "Enviar", Send: 'Enviar',
Config: { Config: {
Reset: "Borrar memoria", Reset: 'Borrar memoria',
SaveAs: "Guardar como máscara", SaveAs: 'Guardar como máscara',
}, },
IsContext: "Prompt predeterminado", IsContext: 'Prompt predeterminado',
}, },
Export: { Export: {
Title: "Compartir historial de chat", Title: 'Compartir historial de chat',
Copy: "Copiar todo", Copy: 'Copiar todo',
Download: "Descargar archivo", Download: 'Descargar archivo',
Share: "Compartir en ShareGPT", Share: 'Compartir en ShareGPT',
MessageFromYou: "Usuario", MessageFromYou: 'Usuario',
MessageFromChatGPT: "ChatGPT", MessageFromChatGPT: 'ChatGPT',
Format: { Format: {
Title: "Formato de exportación", Title: 'Formato de exportación',
SubTitle: "Puedes exportar como texto Markdown o imagen PNG", SubTitle: 'Puedes exportar como texto Markdown o imagen PNG',
}, },
IncludeContext: { IncludeContext: {
Title: "Incluir contexto de máscara", Title: 'Incluir contexto de máscara',
SubTitle: "Mostrar contexto de máscara en los mensajes", SubTitle: 'Mostrar contexto de máscara en los mensajes',
}, },
Steps: { Steps: {
Select: "Seleccionar", Select: 'Seleccionar',
Preview: "Vista previa", Preview: 'Vista previa',
}, },
Image: { Image: {
Toast: "Generando captura de pantalla", Toast: 'Generando captura de pantalla',
Modal: "Mantén presionado o haz clic derecho para guardar la imagen", Modal: 'Mantén presionado o haz clic derecho para guardar la imagen',
}, },
}, },
Select: { Select: {
Search: "Buscar mensajes", Search: 'Buscar mensajes',
All: "Seleccionar todo", All: 'Seleccionar todo',
Latest: "Últimos mensajes", Latest: 'Últimos mensajes',
Clear: "Limpiar selección", Clear: 'Limpiar selección',
}, },
Memory: { Memory: {
Title: "Resumen histórico", Title: 'Resumen histórico',
EmptyContent: EmptyContent:
"El contenido de la conversación es demasiado corto para resumir", 'El contenido de la conversación es demasiado corto para resumir',
Send: "Comprimir automáticamente el historial de chat y enviarlo como contexto", Send: 'Comprimir automáticamente el historial de chat y enviarlo como contexto',
Copy: "Copiar resumen", Copy: 'Copiar resumen',
Reset: "[no usado]", Reset: '[no usado]',
ResetConfirm: "¿Confirmar para borrar el resumen histórico?", ResetConfirm: '¿Confirmar para borrar el resumen histórico?',
}, },
Home: { Home: {
NewChat: "Nueva conversación", NewChat: 'Nueva conversación',
DeleteChat: "¿Confirmar la eliminación de la conversación seleccionada?", DeleteChat: '¿Confirmar la eliminación de la conversación seleccionada?',
DeleteToast: "Conversación eliminada", DeleteToast: 'Conversación eliminada',
Revert: "Deshacer", Revert: 'Deshacer',
}, },
Settings: { Settings: {
Title: "Configuración", Title: 'Configuración',
SubTitle: "Todas las opciones de configuración", SubTitle: 'Todas las opciones de configuración',
Danger: { Danger: {
Reset: { Reset: {
Title: "Restablecer todas las configuraciones", Title: 'Restablecer todas las configuraciones',
SubTitle: SubTitle:
"Restablecer todas las configuraciones a los valores predeterminados", 'Restablecer todas las configuraciones a los valores predeterminados',
Action: "Restablecer ahora", Action: 'Restablecer ahora',
Confirm: "¿Confirmar el restablecimiento de todas las configuraciones?", Confirm: '¿Confirmar el restablecimiento de todas las configuraciones?',
}, },
Clear: { Clear: {
Title: "Eliminar todos los datos", Title: 'Eliminar todos los datos',
SubTitle: "Eliminar todos los chats y datos de configuración", SubTitle: 'Eliminar todos los chats y datos de configuración',
Action: "Eliminar ahora", Action: 'Eliminar ahora',
Confirm: 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: { Lang: {
Name: "Language", // ATENCIÓN: si deseas agregar una nueva traducción, por favor no traduzcas este valor, déjalo como `Language` Name: 'Language', // ATENCIÓN: si deseas agregar una nueva traducción, por favor no traduzcas este valor, déjalo como `Language`
All: "Todos los idiomas", All: 'Todos los idiomas',
}, },
Avatar: "Avatar", Avatar: 'Avatar',
FontSize: { FontSize: {
Title: "Tamaño de fuente", Title: 'Tamaño de fuente',
SubTitle: "Tamaño de la fuente del contenido del chat", SubTitle: 'Tamaño de la fuente del contenido del chat',
}, },
FontFamily: { FontFamily: {
Title: "Fuente del Chat", Title: 'Fuente del Chat',
SubTitle: SubTitle:
"Fuente del contenido del chat, dejar vacío para aplicar la fuente predeterminada global", 'Fuente del contenido del chat, dejar vacío para aplicar la fuente predeterminada global',
Placeholder: "Nombre de la Fuente", Placeholder: 'Nombre de la Fuente',
}, },
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "Inyectar mensajes del sistema", Title: 'Inyectar mensajes del sistema',
SubTitle: 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: { InputTemplate: {
Title: "Preprocesamiento de entrada del usuario", Title: 'Preprocesamiento de entrada del usuario',
SubTitle: "El último mensaje del usuario se rellenará en esta plantilla", SubTitle: 'El último mensaje del usuario se rellenará en esta plantilla',
}, },
Update: { Update: {
Version: (x: string) => `Versión actual: ${x}`, Version: (x: string) => `Versión actual: ${x}`,
IsLatest: "Ya estás en la última versión", IsLatest: 'Ya estás en la última versión',
CheckUpdate: "Buscar actualizaciones", CheckUpdate: 'Buscar actualizaciones',
IsChecking: "Buscando actualizaciones...", IsChecking: 'Buscando actualizaciones...',
FoundUpdate: (x: string) => `Nueva versión encontrada: ${x}`, FoundUpdate: (x: string) => `Nueva versión encontrada: ${x}`,
GoToUpdate: "Ir a actualizar", GoToUpdate: 'Ir a actualizar',
}, },
SendKey: "Tecla de enviar", SendKey: 'Tecla de enviar',
Theme: "Tema", Theme: 'Tema',
TightBorder: "Modo sin borde", TightBorder: 'Modo sin borde',
SendPreviewBubble: { SendPreviewBubble: {
Title: "Vista previa del globo", Title: 'Vista previa del globo',
SubTitle: SubTitle:
"Previsualiza el contenido Markdown en un globo de vista previa", 'Previsualiza el contenido Markdown en un globo de vista previa',
}, },
AutoGenerateTitle: { AutoGenerateTitle: {
Title: "Generar título automáticamente", Title: 'Generar título automáticamente',
SubTitle: "Generar un título adecuado basado en el contenido del chat", SubTitle: 'Generar un título adecuado basado en el contenido del chat',
}, },
Sync: { Sync: {
CloudState: "Datos en la nube", CloudState: 'Datos en la nube',
NotSyncYet: "Aún no se ha sincronizado", NotSyncYet: 'Aún no se ha sincronizado',
Success: "Sincronización exitosa", Success: 'Sincronización exitosa',
Fail: "Sincronización fallida", Fail: 'Sincronización fallida',
Config: { Config: {
Modal: { Modal: {
Title: "Configurar sincronización en la nube", Title: 'Configurar sincronización en la nube',
Check: "Verificar disponibilidad", Check: 'Verificar disponibilidad',
}, },
SyncType: { SyncType: {
Title: "Tipo de sincronización", Title: 'Tipo de sincronización',
SubTitle: "Selecciona el servidor de sincronización preferido", SubTitle: 'Selecciona el servidor de sincronización preferido',
}, },
Proxy: { Proxy: {
Title: "Habilitar proxy", Title: 'Habilitar proxy',
SubTitle: 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: { ProxyUrl: {
Title: "Dirección del proxy", Title: 'Dirección del proxy',
SubTitle: "Solo para el proxy CORS incluido en este proyecto", SubTitle: 'Solo para el proxy CORS incluido en este proyecto',
}, },
WebDav: { WebDav: {
Endpoint: "Dirección WebDAV", Endpoint: 'Dirección WebDAV',
UserName: "Nombre de usuario", UserName: 'Nombre de usuario',
Password: "Contraseña", Password: 'Contraseña',
}, },
UpStash: { UpStash: {
Endpoint: "URL de REST de UpStash Redis", Endpoint: 'URL de REST de UpStash Redis',
UserName: "Nombre de respaldo", UserName: 'Nombre de respaldo',
Password: "Token de REST de UpStash Redis", Password: 'Token de REST de UpStash Redis',
}, },
}, },
LocalState: "Datos locales", LocalState: 'Datos locales',
Overview: (overview: any) => { Overview: (overview: any) => {
return `${overview.chat} conversaciones, ${overview.message} mensajes, ${overview.prompt} prompts, ${overview.mask} máscaras`; return `${overview.chat} conversaciones, ${overview.message} mensajes, ${overview.prompt} prompts, ${overview.mask} máscaras`;
}, },
ImportFailed: "Importación fallida", ImportFailed: 'Importación fallida',
}, },
Mask: { Mask: {
Splash: { Splash: {
Title: "Pantalla de inicio de máscara", Title: 'Pantalla de inicio de máscara',
SubTitle: 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: { Builtin: {
Title: "Ocultar máscaras integradas", Title: 'Ocultar máscaras integradas',
SubTitle: 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: { Prompt: {
Disable: { Disable: {
Title: "Deshabilitar autocompletado de prompts", Title: 'Deshabilitar autocompletado de prompts',
SubTitle: 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) => ListCount: (builtin: number, custom: number) =>
`Integrados ${builtin}, definidos por el usuario ${custom}`, `Integrados ${builtin}, definidos por el usuario ${custom}`,
Edit: "Editar", Edit: 'Editar',
Modal: { Modal: {
Title: "Lista de prompts", Title: 'Lista de prompts',
Add: "Nuevo", Add: 'Nuevo',
Search: "Buscar prompts", Search: 'Buscar prompts',
}, },
EditModal: { EditModal: {
Title: "Editar prompt", Title: 'Editar prompt',
}, },
}, },
HistoryCount: { HistoryCount: {
Title: "Número de mensajes históricos adjuntos", Title: 'Número de mensajes históricos adjuntos',
SubTitle: "Número de mensajes históricos enviados con cada solicitud", SubTitle: 'Número de mensajes históricos enviados con cada solicitud',
}, },
CompressThreshold: { CompressThreshold: {
Title: "Umbral de compresión de mensajes históricos", Title: 'Umbral de compresión de mensajes históricos',
SubTitle: 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: { Usage: {
Title: "Consulta de saldo", Title: 'Consulta de saldo',
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
return `Saldo usado este mes: $${used}, total suscrito: $${total}`; return `Saldo usado este mes: $${used}, total suscrito: $${total}`;
}, },
IsChecking: "Verificando…", IsChecking: 'Verificando…',
Check: "Revisar de nuevo", Check: 'Revisar de nuevo',
NoAccess: 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: { Access: {
SaasStart: { SaasStart: {
Title: "Use NextChat AI", Title: 'Use NextChat AI',
Label: "(The most cost-effective solution)", Label: '(The most cost-effective solution)',
SubTitle: SubTitle:
"Officially maintained by NextChat, zero configuration ready to use, supports the latest large models like OpenAI o1, GPT-4o, and Claude-3.5", '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", ChatNow: 'Chat Now',
}, },
AccessCode: { AccessCode: {
Title: "Contraseña de acceso", Title: 'Contraseña de acceso',
SubTitle: "El administrador ha habilitado el acceso encriptado", SubTitle: 'El administrador ha habilitado el acceso encriptado',
Placeholder: "Introduce la contraseña de acceso", Placeholder: 'Introduce la contraseña de acceso',
}, },
CustomEndpoint: { CustomEndpoint: {
Title: "Interfaz personalizada", Title: 'Interfaz personalizada',
SubTitle: "¿Usar servicios personalizados de Azure u OpenAI?", SubTitle: '¿Usar servicios personalizados de Azure u OpenAI?',
}, },
Provider: { Provider: {
Title: "Proveedor de modelos", Title: 'Proveedor de modelos',
SubTitle: "Cambiar entre diferentes proveedores", SubTitle: 'Cambiar entre diferentes proveedores',
}, },
OpenAI: { OpenAI: {
ApiKey: { ApiKey: {
Title: "Clave API", Title: 'Clave API',
SubTitle: SubTitle:
"Usa una clave API de OpenAI personalizada para omitir la restricción de acceso por contraseña", 'Usa una clave API de OpenAI personalizada para omitir la restricción de acceso por contraseña',
Placeholder: "Clave API de OpenAI", Placeholder: 'Clave API de OpenAI',
}, },
Endpoint: { Endpoint: {
Title: "Dirección del endpoint", Title: 'Dirección del endpoint',
SubTitle: SubTitle:
"Debe incluir http(s):// además de la dirección predeterminada", 'Debe incluir http(s):// además de la dirección predeterminada',
}, },
}, },
Azure: { Azure: {
ApiKey: { ApiKey: {
Title: "Clave de interfaz", Title: 'Clave de interfaz',
SubTitle: SubTitle:
"Usa una clave de Azure personalizada para omitir la restricción de acceso por contraseña", 'Usa una clave de Azure personalizada para omitir la restricción de acceso por contraseña',
Placeholder: "Clave API de Azure", Placeholder: 'Clave API de Azure',
}, },
Endpoint: { Endpoint: {
Title: "Dirección del endpoint", Title: 'Dirección del endpoint',
SubTitle: "Ejemplo:", SubTitle: 'Ejemplo:',
}, },
ApiVerion: { ApiVerion: {
Title: "Versión de la interfaz (versión de api de azure)", Title: 'Versión de la interfaz (versión de api de azure)',
SubTitle: "Selecciona una versión específica", SubTitle: 'Selecciona una versión específica',
}, },
}, },
Anthropic: { Anthropic: {
ApiKey: { ApiKey: {
Title: "Clave de interfaz", Title: 'Clave de interfaz',
SubTitle: SubTitle:
"Usa una clave de Anthropic personalizada para omitir la restricción de acceso por contraseña", 'Usa una clave de Anthropic personalizada para omitir la restricción de acceso por contraseña',
Placeholder: "Clave API de Anthropic", Placeholder: 'Clave API de Anthropic',
}, },
Endpoint: { Endpoint: {
Title: "Dirección del endpoint", Title: 'Dirección del endpoint',
SubTitle: "Ejemplo:", SubTitle: 'Ejemplo:',
}, },
ApiVerion: { ApiVerion: {
Title: "Versión de la interfaz (versión de claude api)", Title: 'Versión de la interfaz (versión de claude api)',
SubTitle: "Selecciona una versión específica de la API", SubTitle: 'Selecciona una versión específica de la API',
}, },
}, },
Google: { Google: {
ApiKey: { ApiKey: {
Title: "Clave API", Title: 'Clave API',
SubTitle: "Obtén tu clave API de Google AI", SubTitle: 'Obtén tu clave API de Google AI',
Placeholder: "Introduce tu clave API de Google AI Studio", Placeholder: 'Introduce tu clave API de Google AI Studio',
}, },
Endpoint: { Endpoint: {
Title: "Dirección del endpoint", Title: 'Dirección del endpoint',
SubTitle: "Ejemplo:", SubTitle: 'Ejemplo:',
}, },
ApiVersion: { ApiVersion: {
Title: "Versión de la API (solo para gemini-pro)", Title: 'Versión de la API (solo para gemini-pro)',
SubTitle: "Selecciona una versión específica de la API", SubTitle: 'Selecciona una versión específica de la API',
}, },
GoogleSafetySettings: { GoogleSafetySettings: {
Title: "Nivel de filtrado de seguridad de Google", Title: 'Nivel de filtrado de seguridad de Google',
SubTitle: "Configura el nivel de filtrado de contenido", SubTitle: 'Configura el nivel de filtrado de contenido',
}, },
}, },
Baidu: { Baidu: {
ApiKey: { ApiKey: {
Title: "Clave API", Title: 'Clave API',
SubTitle: "Usa una clave API de Baidu personalizada", SubTitle: 'Usa una clave API de Baidu personalizada',
Placeholder: "Clave API de Baidu", Placeholder: 'Clave API de Baidu',
}, },
SecretKey: { SecretKey: {
Title: "Clave secreta", Title: 'Clave secreta',
SubTitle: "Usa una clave secreta de Baidu personalizada", SubTitle: 'Usa una clave secreta de Baidu personalizada',
Placeholder: "Clave secreta de Baidu", Placeholder: 'Clave secreta de Baidu',
}, },
Endpoint: { Endpoint: {
Title: "Dirección del endpoint", Title: 'Dirección del endpoint',
SubTitle: SubTitle:
"No admite personalización, dirígete a .env para configurarlo", 'No admite personalización, dirígete a .env para configurarlo',
}, },
}, },
ByteDance: { ByteDance: {
ApiKey: { ApiKey: {
Title: "Clave de interfaz", Title: 'Clave de interfaz',
SubTitle: "Usa una clave API de ByteDance personalizada", SubTitle: 'Usa una clave API de ByteDance personalizada',
Placeholder: "Clave API de ByteDance", Placeholder: 'Clave API de ByteDance',
}, },
Endpoint: { Endpoint: {
Title: "Dirección del endpoint", Title: 'Dirección del endpoint',
SubTitle: "Ejemplo:", SubTitle: 'Ejemplo:',
}, },
}, },
Alibaba: { Alibaba: {
ApiKey: { ApiKey: {
Title: "Clave de interfaz", Title: 'Clave de interfaz',
SubTitle: "Usa una clave API de Alibaba Cloud personalizada", SubTitle: 'Usa una clave API de Alibaba Cloud personalizada',
Placeholder: "Clave API de Alibaba Cloud", Placeholder: 'Clave API de Alibaba Cloud',
}, },
Endpoint: { Endpoint: {
Title: "Dirección del endpoint", Title: 'Dirección del endpoint',
SubTitle: "Ejemplo:", SubTitle: 'Ejemplo:',
}, },
}, },
CustomModel: { CustomModel: {
Title: "Nombre del modelo personalizado", Title: 'Nombre del modelo personalizado',
SubTitle: SubTitle:
"Agrega opciones de modelos personalizados, separados por comas", 'Agrega opciones de modelos personalizados, separados por comas',
}, },
}, },
Model: "Modelo (model)", Model: 'Modelo (model)',
CompressModel: { CompressModel: {
Title: "Modelo de compresión", Title: 'Modelo de compresión',
SubTitle: "Modelo utilizado para comprimir el historial", SubTitle: 'Modelo utilizado para comprimir el historial',
}, },
Temperature: { Temperature: {
Title: "Aleatoriedad (temperature)", Title: 'Aleatoriedad (temperature)',
SubTitle: "Cuanto mayor sea el valor, más aleatorio será el resultado", SubTitle: 'Cuanto mayor sea el valor, más aleatorio será el resultado',
}, },
TopP: { TopP: {
Title: "Muestreo por núcleo (top_p)", Title: 'Muestreo por núcleo (top_p)',
SubTitle: "Similar a la aleatoriedad, pero no cambies ambos a la vez", SubTitle: 'Similar a la aleatoriedad, pero no cambies ambos a la vez',
}, },
MaxTokens: { MaxTokens: {
Title: "Límite de tokens por respuesta (max_tokens)", Title: 'Límite de tokens por respuesta (max_tokens)',
SubTitle: "Número máximo de tokens utilizados en una sola interacción", SubTitle: 'Número máximo de tokens utilizados en una sola interacción',
}, },
PresencePenalty: { PresencePenalty: {
Title: "Novedad de temas (presence_penalty)", Title: 'Novedad de temas (presence_penalty)',
SubTitle: 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: { FrequencyPenalty: {
Title: "Penalización de frecuencia (frequency_penalty)", Title: 'Penalización de frecuencia (frequency_penalty)',
SubTitle: 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: { Store: {
DefaultTopic: "Nuevo chat", DefaultTopic: 'Nuevo chat',
BotHello: "¿En qué puedo ayudarte?", BotHello: '¿En qué puedo ayudarte?',
Error: "Hubo un error, inténtalo de nuevo más tarde", Error: 'Hubo un error, inténtalo de nuevo más tarde',
Prompt: { Prompt: {
History: (content: string) => History: (content: string) =>
"Este es un resumen del chat histórico como referencia: " + content, `Este es un resumen del chat histórico como referencia: ${content}`,
Topic: 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: 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: { Copy: {
Success: "Copiado al portapapeles", Success: 'Copiado al portapapeles',
Failed: "Error al copiar, por favor otorga permisos al portapapeles", Failed: 'Error al copiar, por favor otorga permisos al portapapeles',
}, },
Download: { Download: {
Success: "Contenido descargado en tu directorio.", Success: 'Contenido descargado en tu directorio.',
Failed: "Error al descargar.", Failed: 'Error al descargar.',
}, },
Context: { Context: {
Toast: (x: any) => `Contiene ${x} prompts predefinidos`, Toast: (x: any) => `Contiene ${x} prompts predefinidos`,
Edit: "Configuración del chat actual", Edit: 'Configuración del chat actual',
Add: "Agregar una conversación", Add: 'Agregar una conversación',
Clear: "Contexto borrado", Clear: 'Contexto borrado',
Revert: "Restaurar contexto", Revert: 'Restaurar contexto',
}, },
Plugin: { Plugin: {
Name: "Complemento", Name: 'Complemento',
}, },
FineTuned: { FineTuned: {
Sysmessage: "Eres un asistente", Sysmessage: 'Eres un asistente',
}, },
SearchChat: { SearchChat: {
Name: "Buscar", Name: 'Buscar',
Page: { Page: {
Title: "Buscar en el historial de chat", Title: 'Buscar en el historial de chat',
Search: "Ingrese la palabra clave de búsqueda", Search: 'Ingrese la palabra clave de búsqueda',
NoResult: "No se encontraron resultados", NoResult: 'No se encontraron resultados',
NoData: "Sin datos", NoData: 'Sin datos',
Loading: "Cargando", Loading: 'Cargando',
SubTitle: (count: number) => `Se encontraron ${count} resultados`, SubTitle: (count: number) => `Se encontraron ${count} resultados`,
}, },
Item: { Item: {
View: "Ver", View: 'Ver',
}, },
}, },
Mask: { Mask: {
Name: "Máscara", Name: 'Máscara',
Page: { Page: {
Title: "Máscaras de rol predefinidas", Title: 'Máscaras de rol predefinidas',
SubTitle: (count: number) => `${count} definiciones de rol predefinidas`, SubTitle: (count: number) => `${count} definiciones de rol predefinidas`,
Search: "Buscar máscara de rol", Search: 'Buscar máscara de rol',
Create: "Crear nuevo", Create: 'Crear nuevo',
}, },
Item: { Item: {
Info: (count: number) => `Contiene ${count} conversaciones predefinidas`, Info: (count: number) => `Contiene ${count} conversaciones predefinidas`,
Chat: "Chat", Chat: 'Chat',
View: "Ver", View: 'Ver',
Edit: "Editar", Edit: 'Editar',
Delete: "Eliminar", Delete: 'Eliminar',
DeleteConfirm: "¿Confirmar eliminación?", DeleteConfirm: '¿Confirmar eliminación?',
}, },
EditModal: { EditModal: {
Title: (readonly: boolean) => Title: (readonly: boolean) =>
`Editar máscara predefinida ${readonly ? "solo lectura" : ""}`, `Editar máscara predefinida ${readonly ? 'solo lectura' : ''}`,
Download: "Descargar predefinido", Download: 'Descargar predefinido',
Clone: "Clonar predefinido", Clone: 'Clonar predefinido',
}, },
Config: { Config: {
Avatar: "Avatar del rol", Avatar: 'Avatar del rol',
Name: "Nombre del rol", Name: 'Nombre del rol',
Sync: { Sync: {
Title: "Usar configuración global", Title: 'Usar configuración global',
SubTitle: 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: 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: { HideContext: {
Title: "Ocultar conversaciones predefinidas", Title: 'Ocultar conversaciones predefinidas',
SubTitle: 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: { Share: {
Title: "Compartir esta máscara", Title: 'Compartir esta máscara',
SubTitle: "Generar un enlace directo a esta máscara", SubTitle: 'Generar un enlace directo a esta máscara',
Action: "Copiar enlace", Action: 'Copiar enlace',
}, },
}, },
}, },
NewChat: { NewChat: {
Return: "Regresar", Return: 'Regresar',
Skip: "Comenzar ahora", Skip: 'Comenzar ahora',
NotShow: "No mostrar más", NotShow: 'No mostrar más',
ConfirmNoShow: ConfirmNoShow:
"¿Confirmar desactivación? Puedes reactivar en la configuración en cualquier momento.", '¿Confirmar desactivación? Puedes reactivar en la configuración en cualquier momento.',
Title: "Selecciona una máscara", Title: 'Selecciona una máscara',
SubTitle: "Comienza ahora y colisiona con la mente detrás de la máscara", SubTitle: 'Comienza ahora y colisiona con la mente detrás de la máscara',
More: "Ver todo", More: 'Ver todo',
}, },
URLCommand: { 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: Settings:
"Detectada configuración predefinida en el enlace, ¿deseas autocompletarla?", 'Detectada configuración predefinida en el enlace, ¿deseas autocompletarla?',
}, },
UI: { UI: {
Confirm: "Confirmar", Confirm: 'Confirmar',
Cancel: "Cancelar", Cancel: 'Cancelar',
Close: "Cerrar", Close: 'Cerrar',
Create: "Crear", Create: 'Crear',
Edit: "Editar", Edit: 'Editar',
Export: "Exportar", Export: 'Exportar',
Import: "Importar", Import: 'Importar',
Sync: "Sincronizar", Sync: 'Sincronizar',
Config: "Configurar", Config: 'Configurar',
}, },
Exporter: { Exporter: {
Description: { 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", Model: 'Modelo',
Messages: "Mensajes", Messages: 'Mensajes',
Topic: "Tema", Topic: 'Tema',
Time: "Hora", Time: 'Hora',
}, },
}; };

View File

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

View File

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

View File

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

View File

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

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