mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-23 04:19:23 +08:00
Compare commits
75 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cae4655785 | ||
|
28c12606a4 | ||
|
54df355014 | ||
|
cf50299b14 | ||
|
2c12be62c4 | ||
|
4636a75b5e | ||
|
03756e364a | ||
|
ce1715c79e | ||
|
a62ab3c649 | ||
|
bfb7b988f4 | ||
|
84f41262f5 | ||
|
dda40e29f4 | ||
|
7df868e22a | ||
|
bf5e7aaa48 | ||
|
d76e744eab | ||
|
6f5699fe09 | ||
|
f9d916925e | ||
|
f9258878db | ||
|
ef9e86b50d | ||
|
b21931c667 | ||
|
06de3f5e69 | ||
|
261a8fd83b | ||
|
6527074cde | ||
|
f2485931d9 | ||
|
4f8a0b7711 | ||
|
2dde55050e | ||
|
6aade62ce2 | ||
|
45b88ebb2a | ||
|
dc7159a450 | ||
|
536ace8e10 | ||
|
16b2a3e66e | ||
|
6f135a0cce | ||
|
cf220dd2eb | ||
|
914f4fb862 | ||
|
7bdb68eecf | ||
|
3c510cfaf0 | ||
|
401fa198c9 | ||
|
600df4f63a | ||
|
74eb42c111 | ||
|
9876a1aeca | ||
|
d898ffce23 | ||
|
f1772f4625 | ||
|
9da455a7ea | ||
|
2bd6342309 | ||
|
5e73577088 | ||
|
5156a80738 | ||
|
a9d605ed30 | ||
|
7d1fae32cd | ||
|
9f17b45bc4 | ||
|
a64c9dae42 | ||
|
5fbf4c394c | ||
|
1e5153173c | ||
|
011b52d07d | ||
|
d033168d80 | ||
|
fdca9e59de | ||
|
549a2fd206 | ||
|
a0cd939bfd | ||
|
4f52679ec6 | ||
|
b52e237044 | ||
|
17b329bb63 | ||
|
3a654ba199 | ||
|
5ba3fc9321 | ||
|
a46f08154e | ||
|
0f6ed9c293 | ||
|
295864d36b | ||
|
be6d45e49f | ||
|
22b6987249 | ||
|
64647b0bb3 | ||
|
a5a1f2e8ad | ||
|
8bd39f33e0 | ||
|
be9774943b | ||
|
943214c6a7 | ||
|
ca792669fc | ||
|
78d68a9949 | ||
|
6b2db97347 |
4
.github/workflows/app.yml
vendored
4
.github/workflows/app.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
- name: setup node
|
- name: setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18
|
||||||
- name: get version
|
- name: get version
|
||||||
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
|
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
|
||||||
- name: create release
|
- name: create release
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
- name: setup node
|
- name: setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18
|
||||||
- name: install Rust stable
|
- name: install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
|
14
README.md
14
README.md
@@ -75,7 +75,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
|
|||||||
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
|
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
|
||||||
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
|
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
|
||||||
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
||||||
- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
|
- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
|
||||||
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
|
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
|
||||||
|
|
||||||
## 开发计划
|
## 开发计划
|
||||||
@@ -161,7 +161,7 @@ Access password, separated by comma.
|
|||||||
|
|
||||||
### `OPENAI_API_KEY` (required)
|
### `OPENAI_API_KEY` (required)
|
||||||
|
|
||||||
Your openai api key.
|
Your openai api key, join multiple api keys with comma.
|
||||||
|
|
||||||
### `BASE_URL` (optional)
|
### `BASE_URL` (optional)
|
||||||
|
|
||||||
@@ -216,9 +216,11 @@ If you want to disable parse settings from url, set this to 1.
|
|||||||
### `CUSTOM_MODELS` (optional)
|
### `CUSTOM_MODELS` (optional)
|
||||||
|
|
||||||
> Default: Empty
|
> Default: Empty
|
||||||
> Example: `+llama,+claude-2,-gpt-3.5-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list.
|
> Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`.
|
||||||
|
|
||||||
To control custom models, use `+` to add a custom model, use `-` to hide a model, separated by comma.
|
To control custom models, use `+` to add a custom model, use `-` to hide a model, use `name=displayName` to customize model name, separated by comma.
|
||||||
|
|
||||||
|
User `-all` to disable all default models, `+all` to enable all default models.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -343,6 +345,10 @@ If you want to add a new translation, read this [document](./docs/translation.md
|
|||||||
[@synwith](https://github.com/synwith)
|
[@synwith](https://github.com/synwith)
|
||||||
[@piksonGit](https://github.com/piksonGit)
|
[@piksonGit](https://github.com/piksonGit)
|
||||||
[@ouyangzhiping](https://github.com/ouyangzhiping)
|
[@ouyangzhiping](https://github.com/ouyangzhiping)
|
||||||
|
[@wenjiavv](https://github.com/wenjiavv)
|
||||||
|
[@LeXwDeX](https://github.com/LeXwDeX)
|
||||||
|
[@Licoy](https://github.com/Licoy)
|
||||||
|
[@shangmin2009](https://github.com/shangmin2009)
|
||||||
|
|
||||||
### Contributor
|
### Contributor
|
||||||
|
|
||||||
|
@@ -68,7 +68,7 @@ code1,code2,code3
|
|||||||
|
|
||||||
### `OPENAI_API_KEY` (必填项)
|
### `OPENAI_API_KEY` (必填项)
|
||||||
|
|
||||||
OpanAI 密钥,你在 openai 账户页面申请的 api key。
|
OpanAI 密钥,你在 openai 账户页面申请的 api key,使用英文逗号隔开多个 key,这样可以随机轮询这些 key。
|
||||||
|
|
||||||
### `CODE` (可选)
|
### `CODE` (可选)
|
||||||
|
|
||||||
@@ -122,9 +122,10 @@ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.micro
|
|||||||
|
|
||||||
### `CUSTOM_MODELS` (可选)
|
### `CUSTOM_MODELS` (可选)
|
||||||
|
|
||||||
> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`。
|
> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。
|
||||||
|
> 如果你想先禁用所有模型,再启用指定模型,可以使用 `-all,+gpt-3.5-turbo`,则表示仅启用 `gpt-3.5-turbo`
|
||||||
|
|
||||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,用英文逗号隔开。
|
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.micro
|
|||||||
OPENAI_API_KEY=<your api key here>
|
OPENAI_API_KEY=<your api key here>
|
||||||
|
|
||||||
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
||||||
BASE_URL=https://ab.nextweb.fun/api/proxy
|
BASE_URL=https://b.nextweb.fun/api/proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
### 本地开发
|
### 本地开发
|
||||||
|
@@ -46,6 +46,13 @@ export function auth(req: NextRequest) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serverConfig.hideUserApiKey && !!apiKey) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
msg: "you are not allowed to access openai with your own api key",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// if user does not provide an api key, inject system api key
|
// if user does not provide an api key, inject system api key
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
const serverApiKey = serverConfig.isAzure
|
const serverApiKey = serverConfig.isAzure
|
||||||
|
@@ -18,7 +18,7 @@ export async function requestOpenai(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let baseUrl =
|
let baseUrl =
|
||||||
serverConfig.azureUrl ?? serverConfig.baseUrl ?? OPENAI_BASE_URL;
|
serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL;
|
||||||
|
|
||||||
if (!baseUrl.startsWith("http")) {
|
if (!baseUrl.startsWith("http")) {
|
||||||
baseUrl = `https://${baseUrl}`;
|
baseUrl = `https://${baseUrl}`;
|
||||||
@@ -30,7 +30,10 @@ export async function requestOpenai(req: NextRequest) {
|
|||||||
|
|
||||||
console.log("[Proxy] ", path);
|
console.log("[Proxy] ", path);
|
||||||
console.log("[Base Url]", baseUrl);
|
console.log("[Base Url]", baseUrl);
|
||||||
|
// this fix [Org ID] undefined in server side if not using custom point
|
||||||
|
if (serverConfig.openaiOrgId !== undefined) {
|
||||||
console.log("[Org ID]", serverConfig.openaiOrgId);
|
console.log("[Org ID]", serverConfig.openaiOrgId);
|
||||||
|
}
|
||||||
|
|
||||||
const timeoutId = setTimeout(
|
const timeoutId = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
@@ -81,7 +84,7 @@ export async function requestOpenai(req: NextRequest) {
|
|||||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||||
|
|
||||||
// not undefined and is false
|
// not undefined and is false
|
||||||
if (modelTable[jsonBody?.model ?? ""] === false) {
|
if (modelTable[jsonBody?.model ?? ""].available === false) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: true,
|
error: true,
|
||||||
|
@@ -75,3 +75,4 @@ export const GET = handle;
|
|||||||
export const POST = handle;
|
export const POST = handle;
|
||||||
|
|
||||||
export const runtime = "edge";
|
export const runtime = "edge";
|
||||||
|
export const preferredRegion = ['arn1', 'bom1', 'cdg1', 'cle1', 'cpt1', 'dub1', 'fra1', 'gru1', 'hnd1', 'iad1', 'icn1', 'kix1', 'lhr1', 'pdx1', 'sfo1', 'sin1', 'syd1'];
|
||||||
|
@@ -115,12 +115,35 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
|
|
||||||
if (shouldStream) {
|
if (shouldStream) {
|
||||||
let responseText = "";
|
let responseText = "";
|
||||||
|
let remainText = "";
|
||||||
let finished = false;
|
let finished = false;
|
||||||
|
|
||||||
|
// animate response to make it looks smooth
|
||||||
|
function animateResponseText() {
|
||||||
|
if (finished || controller.signal.aborted) {
|
||||||
|
responseText += remainText;
|
||||||
|
console.log("[Response Animation] finished");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainText.length > 0) {
|
||||||
|
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
||||||
|
const fetchText = remainText.slice(0, fetchCount);
|
||||||
|
responseText += fetchText;
|
||||||
|
remainText = remainText.slice(fetchCount);
|
||||||
|
options.onUpdate?.(responseText, fetchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(animateResponseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// start animaion
|
||||||
|
animateResponseText();
|
||||||
|
|
||||||
const finish = () => {
|
const finish = () => {
|
||||||
if (!finished) {
|
if (!finished) {
|
||||||
options.onFinish(responseText);
|
|
||||||
finished = true;
|
finished = true;
|
||||||
|
options.onFinish(responseText + remainText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,8 +206,7 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
};
|
};
|
||||||
const delta = json.choices[0]?.delta?.content;
|
const delta = json.choices[0]?.delta?.content;
|
||||||
if (delta) {
|
if (delta) {
|
||||||
responseText += delta;
|
remainText += delta;
|
||||||
options.onUpdate?.(responseText, delta);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Request] parse error", text);
|
console.error("[Request] parse error", text);
|
||||||
|
@@ -61,7 +61,10 @@ export function ChatItem(props: {
|
|||||||
{props.narrow ? (
|
{props.narrow ? (
|
||||||
<div className={styles["chat-item-narrow"]}>
|
<div className={styles["chat-item-narrow"]}>
|
||||||
<div className={styles["chat-item-avatar"] + " no-dark"}>
|
<div className={styles["chat-item-avatar"] + " no-dark"}>
|
||||||
<MaskAvatar mask={props.mask} />
|
<MaskAvatar
|
||||||
|
avatar={props.mask.avatar}
|
||||||
|
model={props.mask.modelConfig.model}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["chat-item-narrow-count"]}>
|
<div className={styles["chat-item-narrow-count"]}>
|
||||||
{props.count}
|
{props.count}
|
||||||
|
@@ -431,11 +431,26 @@ export function ChatActions(props: {
|
|||||||
|
|
||||||
// switch model
|
// switch model
|
||||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||||
const models = useAllModels()
|
const allModels = useAllModels();
|
||||||
.filter((m) => m.available)
|
const models = useMemo(
|
||||||
.map((m) => m.name);
|
() => allModels.filter((m) => m.available),
|
||||||
|
[allModels],
|
||||||
|
);
|
||||||
const [showModelSelector, setShowModelSelector] = useState(false);
|
const [showModelSelector, setShowModelSelector] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if current model is not available
|
||||||
|
// switch to first available model
|
||||||
|
const isUnavaliableModel = !models.some((m) => m.name === currentModel);
|
||||||
|
if (isUnavaliableModel && models.length > 0) {
|
||||||
|
const nextModel = models[0].name as ModelType;
|
||||||
|
chatStore.updateCurrentSession(
|
||||||
|
(session) => (session.mask.modelConfig.model = nextModel),
|
||||||
|
);
|
||||||
|
showToast(nextModel);
|
||||||
|
}
|
||||||
|
}, [chatStore, currentModel, models]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["chat-input-actions"]}>
|
<div className={styles["chat-input-actions"]}>
|
||||||
{couldStop && (
|
{couldStop && (
|
||||||
@@ -515,8 +530,8 @@ export function ChatActions(props: {
|
|||||||
<Selector
|
<Selector
|
||||||
defaultSelectedValue={currentModel}
|
defaultSelectedValue={currentModel}
|
||||||
items={models.map((m) => ({
|
items={models.map((m) => ({
|
||||||
title: m,
|
title: m.displayName,
|
||||||
value: m,
|
value: m.name,
|
||||||
}))}
|
}))}
|
||||||
onClose={() => setShowModelSelector(false)}
|
onClose={() => setShowModelSelector(false)}
|
||||||
onSelection={(s) => {
|
onSelection={(s) => {
|
||||||
@@ -1160,7 +1175,12 @@ function _Chat() {
|
|||||||
{["system"].includes(message.role) ? (
|
{["system"].includes(message.role) ? (
|
||||||
<Avatar avatar="2699-fe0f" />
|
<Avatar avatar="2699-fe0f" />
|
||||||
) : (
|
) : (
|
||||||
<MaskAvatar mask={session.mask} />
|
<MaskAvatar
|
||||||
|
avatar={session.mask.avatar}
|
||||||
|
model={
|
||||||
|
message.model || session.mask.modelConfig.model
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@@ -186,7 +186,8 @@
|
|||||||
box-shadow: var(--card-shadow);
|
box-shadow: var(--card-shadow);
|
||||||
border: var(--border-in-light);
|
border: var(--border-in-light);
|
||||||
|
|
||||||
*:not(li) {
|
code,
|
||||||
|
pre {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
import styles from "./exporter.module.scss";
|
import styles from "./exporter.module.scss";
|
||||||
import {
|
import {
|
||||||
@@ -27,7 +27,7 @@ import { Avatar } from "./emoji";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import NextImage from "next/image";
|
import NextImage from "next/image";
|
||||||
|
|
||||||
import { toBlob, toJpeg, toPng } from "html-to-image";
|
import { toBlob, toPng } from "html-to-image";
|
||||||
import { DEFAULT_MASK_AVATAR } from "../store/mask";
|
import { DEFAULT_MASK_AVATAR } from "../store/mask";
|
||||||
import { api } from "../client/api";
|
import { api } from "../client/api";
|
||||||
import { prettyObject } from "../utils/format";
|
import { prettyObject } from "../utils/format";
|
||||||
@@ -41,7 +41,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
|||||||
export function ExportMessageModal(props: { onClose: () => void }) {
|
export function ExportMessageModal(props: { onClose: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div className="modal-mask">
|
<div className="modal-mask">
|
||||||
<Modal title={Locale.Export.Title} onClose={props.onClose}>
|
<Modal
|
||||||
|
title={Locale.Export.Title}
|
||||||
|
onClose={props.onClose}
|
||||||
|
footer={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 14,
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Locale.Exporter.Description.Title}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div style={{ minHeight: "40vh" }}>
|
<div style={{ minHeight: "40vh" }}>
|
||||||
<MessageExporter />
|
<MessageExporter />
|
||||||
</div>
|
</div>
|
||||||
@@ -149,7 +164,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, i) => selection.has(m.id)));
|
ret.push(...session.messages.filter((m) => selection.has(m.id)));
|
||||||
return ret;
|
return ret;
|
||||||
}, [
|
}, [
|
||||||
exportConfig.includeContext,
|
exportConfig.includeContext,
|
||||||
@@ -260,7 +275,8 @@ export function RenderExport(props: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
props.onRender(renderMsgs);
|
props.onRender(renderMsgs);
|
||||||
});
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={domRef}>
|
<div ref={domRef}>
|
||||||
@@ -604,8 +620,6 @@ export function MarkdownPreviewer(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// modified by BackTrackZ now it's looks better
|
|
||||||
|
|
||||||
export function JsonPreviewer(props: {
|
export function JsonPreviewer(props: {
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
topic: string;
|
topic: string;
|
||||||
|
@@ -5,13 +5,13 @@ import RemarkBreaks from "remark-breaks";
|
|||||||
import RehypeKatex from "rehype-katex";
|
import RehypeKatex from "rehype-katex";
|
||||||
import RemarkGfm from "remark-gfm";
|
import RemarkGfm from "remark-gfm";
|
||||||
import RehypeHighlight from "rehype-highlight";
|
import RehypeHighlight from "rehype-highlight";
|
||||||
import { useRef, useState, RefObject, useEffect } from "react";
|
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
|
||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard } from "../utils";
|
||||||
import mermaid from "mermaid";
|
import mermaid from "mermaid";
|
||||||
|
|
||||||
import LoadingIcon from "../icons/three-dots.svg";
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { showImageModal } from "./ui-lib";
|
import { showImageModal } from "./ui-lib";
|
||||||
|
|
||||||
export function Mermaid(props: { code: string }) {
|
export function Mermaid(props: { code: string }) {
|
||||||
@@ -99,7 +99,29 @@ export function PreCode(props: { children: any }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeDollarNumber(text: string) {
|
||||||
|
let escapedText = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < text.length; i += 1) {
|
||||||
|
let char = text[i];
|
||||||
|
const nextChar = text[i + 1] || " ";
|
||||||
|
|
||||||
|
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
|
||||||
|
char = "\\$";
|
||||||
|
}
|
||||||
|
|
||||||
|
escapedText += char;
|
||||||
|
}
|
||||||
|
|
||||||
|
return escapedText;
|
||||||
|
}
|
||||||
|
|
||||||
function _MarkDownContent(props: { content: string }) {
|
function _MarkDownContent(props: { content: string }) {
|
||||||
|
const escapedContent = useMemo(
|
||||||
|
() => escapeDollarNumber(props.content),
|
||||||
|
[props.content],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||||
@@ -124,7 +146,7 @@ function _MarkDownContent(props: { content: string }) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.content}
|
{escapedContent}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,7 @@ import {
|
|||||||
ChatMessage,
|
ChatMessage,
|
||||||
createMessage,
|
createMessage,
|
||||||
ModelConfig,
|
ModelConfig,
|
||||||
|
ModelType,
|
||||||
useAppConfig,
|
useAppConfig,
|
||||||
useChatStore,
|
useChatStore,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
@@ -58,11 +59,11 @@ function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MaskAvatar(props: { mask: Mask }) {
|
export function MaskAvatar(props: { avatar: string; model?: ModelType }) {
|
||||||
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
|
return props.avatar !== DEFAULT_MASK_AVATAR ? (
|
||||||
<Avatar avatar={props.mask.avatar} />
|
<Avatar avatar={props.avatar} />
|
||||||
) : (
|
) : (
|
||||||
<Avatar model={props.mask.modelConfig.model} />
|
<Avatar model={props.model} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +124,10 @@ export function MaskConfig(props: {
|
|||||||
onClick={() => setShowPicker(true)}
|
onClick={() => setShowPicker(true)}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<MaskAvatar mask={props.mask} />
|
<MaskAvatar
|
||||||
|
avatar={props.mask.avatar}
|
||||||
|
model={props.mask.modelConfig.model}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -398,7 +402,7 @@ export function MaskPage() {
|
|||||||
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);
|
||||||
} else {
|
} else {
|
||||||
@@ -523,7 +527,7 @@ export function MaskPage() {
|
|||||||
<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 mask={m} />
|
<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>
|
||||||
|
@@ -58,8 +58,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
flex-grow: 1;
|
flex: 1;
|
||||||
max-width: calc(100% - 40px);
|
max-width: calc(100% - 80px);
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -71,6 +71,12 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||||
import { Updater } from "../typing";
|
import { Updater } from "../typing";
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
@@ -73,11 +73,23 @@ export function MessageSelector(props: {
|
|||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const session = chatStore.currentSession();
|
const session = chatStore.currentSession();
|
||||||
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
|
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
|
||||||
const messages = session.messages.filter(
|
const allMessages = useMemo(() => {
|
||||||
|
let startIndex = Math.max(0, session.clearContextIndex ?? 0);
|
||||||
|
if (startIndex === session.messages.length - 1) {
|
||||||
|
startIndex = 0;
|
||||||
|
}
|
||||||
|
return session.messages.slice(startIndex);
|
||||||
|
}, [session.messages, session.clearContextIndex]);
|
||||||
|
|
||||||
|
const messages = useMemo(
|
||||||
|
() =>
|
||||||
|
allMessages.filter(
|
||||||
(m, i) =>
|
(m, i) =>
|
||||||
m.id && // message must have id
|
m.id && // message must have id
|
||||||
isValid(m) &&
|
isValid(m) &&
|
||||||
(i >= session.messages.length - 1 || isValid(session.messages[i + 1])),
|
(i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
|
||||||
|
),
|
||||||
|
[allMessages],
|
||||||
);
|
);
|
||||||
const messageCount = messages.length;
|
const messageCount = messages.length;
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
@@ -176,6 +188,8 @@ export function MessageSelector(props: {
|
|||||||
<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 isSelected = props.selection.has(id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -185,7 +199,6 @@ export function MessageSelector(props: {
|
|||||||
key={i}
|
key={i}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.updateSelection((selection) => {
|
props.updateSelection((selection) => {
|
||||||
const id = m.id ?? i;
|
|
||||||
selection.has(id) ? selection.delete(id) : selection.add(id);
|
selection.has(id) ? selection.delete(id) : selection.add(id);
|
||||||
});
|
});
|
||||||
onClickIndex(i);
|
onClickIndex(i);
|
||||||
@@ -195,7 +208,10 @@ export function MessageSelector(props: {
|
|||||||
{m.role === "user" ? (
|
{m.role === "user" ? (
|
||||||
<Avatar avatar={config.avatar}></Avatar>
|
<Avatar avatar={config.avatar}></Avatar>
|
||||||
) : (
|
) : (
|
||||||
<MaskAvatar mask={session.mask} />
|
<MaskAvatar
|
||||||
|
avatar={session.mask.avatar}
|
||||||
|
model={m.model || session.mask.modelConfig.model}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["body"]}>
|
<div className={styles["body"]}>
|
||||||
@@ -206,6 +222,10 @@ export function MessageSelector(props: {
|
|||||||
{m.content}
|
{m.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["checkbox"]}>
|
||||||
|
<input type="checkbox" checked={isSelected}></input>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@@ -25,9 +25,11 @@ export function ModelConfigList(props: {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{allModels.map((v, i) => (
|
{allModels
|
||||||
<option value={v.name} key={i} disabled={!v.available}>
|
.filter((v) => v.available)
|
||||||
{v.name}
|
.map((v, i) => (
|
||||||
|
<option value={v.name} key={i}>
|
||||||
|
{v.displayName}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
@@ -17,21 +17,13 @@ import { useCommand } from "../command";
|
|||||||
import { showConfirm } from "./ui-lib";
|
import { showConfirm } from "./ui-lib";
|
||||||
import { BUILTIN_MASK_STORE } from "../masks";
|
import { BUILTIN_MASK_STORE } from "../masks";
|
||||||
|
|
||||||
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
|
|
||||||
const xmin = Math.max(aRect.x, bRect.x);
|
|
||||||
const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
|
|
||||||
const ymin = Math.max(aRect.y, bRect.y);
|
|
||||||
const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
|
|
||||||
const width = xmax - xmin;
|
|
||||||
const height = ymax - ymin;
|
|
||||||
const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
|
|
||||||
return intersectionArea;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 mask={props.mask} />
|
<MaskAvatar
|
||||||
|
avatar={props.mask.avatar}
|
||||||
|
model={props.mask.modelConfig.model}
|
||||||
|
/>
|
||||||
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
|
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -635,6 +635,11 @@ export function Settings() {
|
|||||||
navigate(Path.Home);
|
navigate(Path.Home);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (clientConfig?.isApp) { // Force to set custom endpoint to true if it's app
|
||||||
|
accessStore.update((state) => {
|
||||||
|
state.useCustomConfig = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
document.addEventListener("keydown", keydownEvent);
|
document.addEventListener("keydown", keydownEvent);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", keydownEvent);
|
document.removeEventListener("keydown", keydownEvent);
|
||||||
@@ -909,6 +914,9 @@ export function Settings() {
|
|||||||
|
|
||||||
{!accessStore.hideUserApiKey && (
|
{!accessStore.hideUserApiKey && (
|
||||||
<>
|
<>
|
||||||
|
{
|
||||||
|
// Conditionally render the following ListItem based on clientConfig.isApp
|
||||||
|
!clientConfig?.isApp && ( // only show if isApp is false
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.Access.CustomEndpoint.Title}
|
title={Locale.Settings.Access.CustomEndpoint.Title}
|
||||||
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
|
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
|
||||||
@@ -924,6 +932,8 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
{accessStore.useCustomConfig && (
|
{accessStore.useCustomConfig && (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem
|
||||||
@@ -1052,7 +1062,7 @@ export function Settings() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!shouldHideBalanceQuery ? (
|
{!shouldHideBalanceQuery && !clientConfig?.isApp ? (
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.Usage.Title}
|
title={Locale.Settings.Usage.Title}
|
||||||
subTitle={
|
subTitle={
|
||||||
|
@@ -97,8 +97,9 @@ export function Loading() {
|
|||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
title: string;
|
title: string;
|
||||||
children?: any;
|
children?: any;
|
||||||
actions?: JSX.Element[];
|
actions?: React.ReactNode[];
|
||||||
defaultMax?: boolean;
|
defaultMax?: boolean;
|
||||||
|
footer?: React.ReactNode;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
export function Modal(props: ModalProps) {
|
export function Modal(props: ModalProps) {
|
||||||
@@ -147,6 +148,7 @@ export function Modal(props: ModalProps) {
|
|||||||
<div className={styles["modal-content"]}>{props.children}</div>
|
<div className={styles["modal-content"]}>{props.children}</div>
|
||||||
|
|
||||||
<div className={styles["modal-footer"]}>
|
<div className={styles["modal-footer"]}>
|
||||||
|
{props.footer}
|
||||||
<div className={styles["modal-actions"]}>
|
<div className={styles["modal-actions"]}>
|
||||||
{props.actions?.map((action, i) => (
|
{props.actions?.map((action, i) => (
|
||||||
<div key={i} className={styles["modal-action"]}>
|
<div key={i} className={styles["modal-action"]}>
|
||||||
|
@@ -62,9 +62,17 @@ export const getServerSideConfig = () => {
|
|||||||
|
|
||||||
const isAzure = !!process.env.AZURE_URL;
|
const isAzure = !!process.env.AZURE_URL;
|
||||||
|
|
||||||
|
const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||||
|
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||||
|
const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||||
|
const apiKey = apiKeys[randomIndex];
|
||||||
|
console.log(
|
||||||
|
`[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baseUrl: process.env.BASE_URL,
|
baseUrl: process.env.BASE_URL,
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
apiKey,
|
||||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||||
|
|
||||||
isAzure,
|
isAzure,
|
||||||
|
@@ -8,7 +8,7 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c
|
|||||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||||
|
|
||||||
export const DEFAULT_CORS_HOST = "https://ab.nextweb.fun";
|
export const DEFAULT_CORS_HOST = "https://a.nextweb.fun";
|
||||||
export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
|
export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
|
||||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||||
|
|
||||||
@@ -84,6 +84,8 @@ You are ChatGPT, a large language model trained by OpenAI.
|
|||||||
Knowledge cutoff: {{cutoff}}
|
Knowledge cutoff: {{cutoff}}
|
||||||
Current model: {{model}}
|
Current model: {{model}}
|
||||||
Current time: {{time}}
|
Current time: {{time}}
|
||||||
|
Latex inline: $x^2$
|
||||||
|
Latex block: $$e=mc^2$$
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
|
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
|
||||||
|
@@ -85,8 +85,8 @@ const cn = {
|
|||||||
Copy: "全部复制",
|
Copy: "全部复制",
|
||||||
Download: "下载文件",
|
Download: "下载文件",
|
||||||
Share: "分享到 ShareGPT",
|
Share: "分享到 ShareGPT",
|
||||||
MessageFromYou: "来自你的消息",
|
MessageFromYou: "用户",
|
||||||
MessageFromChatGPT: "来自 ChatGPT 的消息",
|
MessageFromChatGPT: "ChatGPT",
|
||||||
Format: {
|
Format: {
|
||||||
Title: "导出格式",
|
Title: "导出格式",
|
||||||
SubTitle: "可以导出 Markdown 文本或者 PNG 图片",
|
SubTitle: "可以导出 Markdown 文本或者 PNG 图片",
|
||||||
@@ -441,6 +441,9 @@ const cn = {
|
|||||||
Config: "配置",
|
Config: "配置",
|
||||||
},
|
},
|
||||||
Exporter: {
|
Exporter: {
|
||||||
|
Description : {
|
||||||
|
Title: "只有清除上下文之后的消息会被展示"
|
||||||
|
},
|
||||||
Model: "模型",
|
Model: "模型",
|
||||||
Messages: "消息",
|
Messages: "消息",
|
||||||
Topic: "主题",
|
Topic: "主题",
|
||||||
|
@@ -442,6 +442,9 @@ const en: LocaleType = {
|
|||||||
Config: "Config",
|
Config: "Config",
|
||||||
},
|
},
|
||||||
Exporter: {
|
Exporter: {
|
||||||
|
Description: {
|
||||||
|
Title: "Only messages after clearing the context will be displayed"
|
||||||
|
},
|
||||||
Model: "Model",
|
Model: "Model",
|
||||||
Messages: "Messages",
|
Messages: "Messages",
|
||||||
Topic: "Topic",
|
Topic: "Topic",
|
||||||
|
@@ -368,6 +368,9 @@ const id: PartialLocaleType = {
|
|||||||
Edit: "Edit",
|
Edit: "Edit",
|
||||||
},
|
},
|
||||||
Exporter: {
|
Exporter: {
|
||||||
|
Description: {
|
||||||
|
Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan"
|
||||||
|
},
|
||||||
Model: "Model",
|
Model: "Model",
|
||||||
Messages: "Pesan",
|
Messages: "Pesan",
|
||||||
Topic: "Topik",
|
Topic: "Topik",
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import cn from "./cn";
|
import cn from "./cn";
|
||||||
import en from "./en";
|
import en from "./en";
|
||||||
|
import pt from "./pt";
|
||||||
import tw from "./tw";
|
import tw from "./tw";
|
||||||
import id from "./id";
|
import id from "./id";
|
||||||
import fr from "./fr";
|
import fr from "./fr";
|
||||||
@@ -24,6 +25,7 @@ const ALL_LANGS = {
|
|||||||
cn,
|
cn,
|
||||||
en,
|
en,
|
||||||
tw,
|
tw,
|
||||||
|
pt,
|
||||||
jp,
|
jp,
|
||||||
ko,
|
ko,
|
||||||
id,
|
id,
|
||||||
@@ -47,6 +49,7 @@ 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",
|
||||||
tw: "繁體中文",
|
tw: "繁體中文",
|
||||||
jp: "日本語",
|
jp: "日本語",
|
||||||
ko: "한국어",
|
ko: "한국어",
|
||||||
|
466
app/locales/pt.ts
Normal file
466
app/locales/pt.ts
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
import { SubmitKey } from "../store/config";
|
||||||
|
import { PartialLocaleType } from "../locales/index";
|
||||||
|
import { getClientConfig } from "../config/client";
|
||||||
|
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
|
||||||
|
const pt: PartialLocaleType = {
|
||||||
|
WIP: "Em breve...",
|
||||||
|
Error: {
|
||||||
|
Unauthorized: isApp
|
||||||
|
? "Chave API inválida, por favor verifique em [Configurações](/#/settings)."
|
||||||
|
: "Acesso não autorizado, por favor insira o código de acesso em [auth](/#/auth) ou insira sua Chave API OpenAI.",
|
||||||
|
},
|
||||||
|
Auth: {
|
||||||
|
Title: "Necessário Código de Acesso",
|
||||||
|
Tips: "Por favor, insira o código de acesso abaixo",
|
||||||
|
SubTips: "Ou insira sua Chave API OpenAI",
|
||||||
|
Input: "código de acesso",
|
||||||
|
Confirm: "Confirmar",
|
||||||
|
Later: "Depois",
|
||||||
|
},
|
||||||
|
ChatItem: {
|
||||||
|
ChatItemCount: (count: number) => `${count} mensagens`,
|
||||||
|
},
|
||||||
|
Chat: {
|
||||||
|
SubTitle: (count: number) => `${count} mensagens`,
|
||||||
|
EditMessage: {
|
||||||
|
Title: "Editar Todas as Mensagens",
|
||||||
|
Topic: {
|
||||||
|
Title: "Tópico",
|
||||||
|
SubTitle: "Mudar o tópico atual",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Actions: {
|
||||||
|
ChatList: "Ir Para Lista de Chat",
|
||||||
|
CompressedHistory: "Prompt de Memória Histórica Comprimida",
|
||||||
|
Export: "Exportar Todas as Mensagens como Markdown",
|
||||||
|
Copy: "Copiar",
|
||||||
|
Stop: "Parar",
|
||||||
|
Retry: "Tentar Novamente",
|
||||||
|
Pin: "Fixar",
|
||||||
|
PinToastContent: "Fixada 1 mensagem para prompts contextuais",
|
||||||
|
PinToastAction: "Visualizar",
|
||||||
|
Delete: "Deletar",
|
||||||
|
Edit: "Editar",
|
||||||
|
},
|
||||||
|
Commands: {
|
||||||
|
new: "Iniciar um novo chat",
|
||||||
|
newm: "Iniciar um novo chat com máscara",
|
||||||
|
next: "Próximo Chat",
|
||||||
|
prev: "Chat Anterior",
|
||||||
|
clear: "Limpar Contexto",
|
||||||
|
del: "Deletar Chat",
|
||||||
|
},
|
||||||
|
InputActions: {
|
||||||
|
Stop: "Parar",
|
||||||
|
ToBottom: "Para o Mais Recente",
|
||||||
|
Theme: {
|
||||||
|
auto: "Automático",
|
||||||
|
light: "Tema Claro",
|
||||||
|
dark: "Tema Escuro",
|
||||||
|
},
|
||||||
|
Prompt: "Prompts",
|
||||||
|
Masks: "Máscaras",
|
||||||
|
Clear: "Limpar Contexto",
|
||||||
|
Settings: "Configurações",
|
||||||
|
},
|
||||||
|
Rename: "Renomear Chat",
|
||||||
|
Typing: "Digitando…",
|
||||||
|
Input: (submitKey: string) => {
|
||||||
|
var inputHints = `${submitKey} para enviar`;
|
||||||
|
if (submitKey === String(SubmitKey.Enter)) {
|
||||||
|
inputHints += ", Shift + Enter para quebrar linha";
|
||||||
|
}
|
||||||
|
return inputHints + ", / para buscar prompts, : para usar comandos";
|
||||||
|
},
|
||||||
|
Send: "Enviar",
|
||||||
|
Config: {
|
||||||
|
Reset: "Redefinir para Padrão",
|
||||||
|
SaveAs: "Salvar como Máscara",
|
||||||
|
},
|
||||||
|
IsContext: "Prompt Contextual",
|
||||||
|
},
|
||||||
|
Export: {
|
||||||
|
Title: "Exportar Mensagens",
|
||||||
|
Copy: "Copiar Tudo",
|
||||||
|
Download: "Baixar",
|
||||||
|
MessageFromYou: "Mensagem De Você",
|
||||||
|
MessageFromChatGPT: "Mensagem De ChatGPT",
|
||||||
|
Share: "Compartilhar para ShareGPT",
|
||||||
|
Format: {
|
||||||
|
Title: "Formato de Exportação",
|
||||||
|
SubTitle: "Markdown ou Imagem PNG",
|
||||||
|
},
|
||||||
|
IncludeContext: {
|
||||||
|
Title: "Incluindo Contexto",
|
||||||
|
SubTitle: "Exportar prompts de contexto na máscara ou não",
|
||||||
|
},
|
||||||
|
Steps: {
|
||||||
|
Select: "Selecionar",
|
||||||
|
Preview: "Pré-visualizar",
|
||||||
|
},
|
||||||
|
Image: {
|
||||||
|
Toast: "Capturando Imagem...",
|
||||||
|
Modal:
|
||||||
|
"Pressione longamente ou clique com o botão direito para salvar a imagem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Select: {
|
||||||
|
Search: "Buscar",
|
||||||
|
All: "Selecionar Tudo",
|
||||||
|
Latest: "Selecionar Mais Recente",
|
||||||
|
Clear: "Limpar",
|
||||||
|
},
|
||||||
|
Memory: {
|
||||||
|
Title: "Prompt de Memória",
|
||||||
|
EmptyContent: "Nada ainda.",
|
||||||
|
Send: "Enviar Memória",
|
||||||
|
Copy: "Copiar Memória",
|
||||||
|
Reset: "Resetar Sessão",
|
||||||
|
ResetConfirm:
|
||||||
|
"Resetar irá limpar o histórico de conversa atual e a memória histórica. Você tem certeza que quer resetar?",
|
||||||
|
},
|
||||||
|
Home: {
|
||||||
|
NewChat: "Novo Chat",
|
||||||
|
DeleteChat: "Confirmar para deletar a conversa selecionada?",
|
||||||
|
DeleteToast: "Chat Deletado",
|
||||||
|
Revert: "Reverter",
|
||||||
|
},
|
||||||
|
Settings: {
|
||||||
|
Title: "Configurações",
|
||||||
|
SubTitle: "Todas as Configurações",
|
||||||
|
Danger: {
|
||||||
|
Reset: {
|
||||||
|
Title: "Resetar Todas as Configurações",
|
||||||
|
SubTitle: "Resetar todos os itens de configuração para o padrão",
|
||||||
|
Action: "Resetar",
|
||||||
|
Confirm: "Confirmar para resetar todas as configurações para o padrão?",
|
||||||
|
},
|
||||||
|
Clear: {
|
||||||
|
Title: "Limpar Todos os Dados",
|
||||||
|
SubTitle: "Limpar todas as mensagens e configurações",
|
||||||
|
Action: "Limpar",
|
||||||
|
Confirm: "Confirmar para limpar todas as mensagens e configurações?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Lang: {
|
||||||
|
Name: "Language",
|
||||||
|
All: "Todos os Idiomas",
|
||||||
|
},
|
||||||
|
Avatar: "Avatar",
|
||||||
|
FontSize: {
|
||||||
|
Title: "Tamanho da Fonte",
|
||||||
|
SubTitle: "Ajustar o tamanho da fonte do conteúdo do chat",
|
||||||
|
},
|
||||||
|
InjectSystemPrompts: {
|
||||||
|
Title: "Inserir Prompts de Sistema",
|
||||||
|
SubTitle: "Inserir um prompt de sistema global para cada requisição",
|
||||||
|
},
|
||||||
|
InputTemplate: {
|
||||||
|
Title: "Modelo de Entrada",
|
||||||
|
SubTitle: "A mensagem mais recente será preenchida neste modelo",
|
||||||
|
},
|
||||||
|
|
||||||
|
Update: {
|
||||||
|
Version: (x: string) => `Versão: ${x}`,
|
||||||
|
IsLatest: "Última versão",
|
||||||
|
CheckUpdate: "Verificar Atualização",
|
||||||
|
IsChecking: "Verificando atualização...",
|
||||||
|
FoundUpdate: (x: string) => `Nova versão encontrada: ${x}`,
|
||||||
|
GoToUpdate: "Atualizar",
|
||||||
|
},
|
||||||
|
SendKey: "Tecla de Envio",
|
||||||
|
Theme: "Tema",
|
||||||
|
TightBorder: "Borda Ajustada",
|
||||||
|
SendPreviewBubble: {
|
||||||
|
Title: "Bolha de Pré-visualização de Envio",
|
||||||
|
SubTitle: "Pré-visualizar markdown na bolha",
|
||||||
|
},
|
||||||
|
AutoGenerateTitle: {
|
||||||
|
Title: "Gerar Título Automaticamente",
|
||||||
|
SubTitle: "Gerar um título adequado baseado no conteúdo da conversa",
|
||||||
|
},
|
||||||
|
Sync: {
|
||||||
|
CloudState: "Última Atualização",
|
||||||
|
NotSyncYet: "Ainda não sincronizado",
|
||||||
|
Success: "Sincronização bem sucedida",
|
||||||
|
Fail: "Falha na sincronização",
|
||||||
|
|
||||||
|
Config: {
|
||||||
|
Modal: {
|
||||||
|
Title: "Configurar Sincronização",
|
||||||
|
Check: "Verificar Conexão",
|
||||||
|
},
|
||||||
|
SyncType: {
|
||||||
|
Title: "Tipo de Sincronização",
|
||||||
|
SubTitle: "Escolha seu serviço de sincronização favorito",
|
||||||
|
},
|
||||||
|
Proxy: {
|
||||||
|
Title: "Habilitar Proxy CORS",
|
||||||
|
SubTitle: "Habilitar um proxy para evitar restrições de cross-origin",
|
||||||
|
},
|
||||||
|
ProxyUrl: {
|
||||||
|
Title: "Endpoint de Proxy",
|
||||||
|
SubTitle: "Apenas aplicável ao proxy CORS embutido para este projeto",
|
||||||
|
},
|
||||||
|
|
||||||
|
WebDav: {
|
||||||
|
Endpoint: "Endpoint WebDAV",
|
||||||
|
UserName: "Nome de Usuário",
|
||||||
|
Password: "Senha",
|
||||||
|
},
|
||||||
|
|
||||||
|
UpStash: {
|
||||||
|
Endpoint: "URL REST Redis UpStash",
|
||||||
|
UserName: "Nome do Backup",
|
||||||
|
Password: "Token REST Redis UpStash",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
LocalState: "Dados Locais",
|
||||||
|
Overview: (overview: any) => {
|
||||||
|
return `${overview.chat} chats,${overview.message} mensagens,${overview.prompt} prompts,${overview.mask} máscaras`;
|
||||||
|
},
|
||||||
|
ImportFailed: "Falha ao importar do arquivo",
|
||||||
|
},
|
||||||
|
Mask: {
|
||||||
|
Splash: {
|
||||||
|
Title: "Tela de Início da Máscara",
|
||||||
|
SubTitle:
|
||||||
|
"Mostrar uma tela de início da máscara antes de iniciar novo chat",
|
||||||
|
},
|
||||||
|
Builtin: {
|
||||||
|
Title: "Esconder Máscaras Embutidas",
|
||||||
|
SubTitle: "Esconder máscaras embutidas na lista de máscaras",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Prompt: {
|
||||||
|
Disable: {
|
||||||
|
Title: "Desabilitar auto-completar",
|
||||||
|
SubTitle: "Digite / para acionar auto-completar",
|
||||||
|
},
|
||||||
|
List: "Lista de Prompts",
|
||||||
|
ListCount: (builtin: number, custom: number) =>
|
||||||
|
`${builtin} embutidos, ${custom} definidos pelo usuário`,
|
||||||
|
Edit: "Editar",
|
||||||
|
Modal: {
|
||||||
|
Title: "Lista de Prompts",
|
||||||
|
Add: "Adicionar Um",
|
||||||
|
Search: "Buscar Prompts",
|
||||||
|
},
|
||||||
|
EditModal: {
|
||||||
|
Title: "Editar Prompt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HistoryCount: {
|
||||||
|
Title: "Contagem de Mensagens Anexadas",
|
||||||
|
SubTitle: "Número de mensagens enviadas anexadas por requisição",
|
||||||
|
},
|
||||||
|
CompressThreshold: {
|
||||||
|
Title: "Limite de Compressão de Histórico",
|
||||||
|
SubTitle:
|
||||||
|
"Irá comprimir se o comprimento das mensagens não comprimidas exceder o valor",
|
||||||
|
},
|
||||||
|
|
||||||
|
Usage: {
|
||||||
|
Title: "Saldo da Conta",
|
||||||
|
SubTitle(used: any, total: any) {
|
||||||
|
return `Usado este mês ${used}, assinatura ${total}`;
|
||||||
|
},
|
||||||
|
IsChecking: "Verificando...",
|
||||||
|
Check: "Verificar",
|
||||||
|
NoAccess: "Insira a Chave API para verificar o saldo",
|
||||||
|
},
|
||||||
|
Access: {
|
||||||
|
AccessCode: {
|
||||||
|
Title: "Código de Acesso",
|
||||||
|
SubTitle: "Controle de Acesso Habilitado",
|
||||||
|
Placeholder: "Insira o Código",
|
||||||
|
},
|
||||||
|
CustomEndpoint: {
|
||||||
|
Title: "Endpoint Personalizado",
|
||||||
|
SubTitle: "Use serviço personalizado Azure ou OpenAI",
|
||||||
|
},
|
||||||
|
Provider: {
|
||||||
|
Title: "Provedor do Modelo",
|
||||||
|
SubTitle: "Selecione Azure ou OpenAI",
|
||||||
|
},
|
||||||
|
OpenAI: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Chave API OpenAI",
|
||||||
|
SubTitle: "Usar Chave API OpenAI personalizada",
|
||||||
|
Placeholder: "sk-xxx",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint OpenAI",
|
||||||
|
SubTitle:
|
||||||
|
"Deve começar com http(s):// ou usar /api/openai como padrão",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Azure: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Chave API Azure",
|
||||||
|
SubTitle: "Verifique sua chave API do console Azure",
|
||||||
|
Placeholder: "Chave API Azure",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Azure",
|
||||||
|
SubTitle: "Exemplo: ",
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "Versão API Azure",
|
||||||
|
SubTitle: "Verifique sua versão API do console Azure",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CustomModel: {
|
||||||
|
Title: "Modelos Personalizados",
|
||||||
|
SubTitle: "Opções de modelo personalizado, separados por vírgula",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Model: "Modelo",
|
||||||
|
Temperature: {
|
||||||
|
Title: "Temperatura",
|
||||||
|
SubTitle: "Um valor maior torna a saída mais aleatória",
|
||||||
|
},
|
||||||
|
TopP: {
|
||||||
|
Title: "Top P",
|
||||||
|
SubTitle: "Não altere este valor junto com a temperatura",
|
||||||
|
},
|
||||||
|
MaxTokens: {
|
||||||
|
Title: "Máximo de Tokens",
|
||||||
|
SubTitle: "Comprimento máximo de tokens de entrada e tokens gerados",
|
||||||
|
},
|
||||||
|
PresencePenalty: {
|
||||||
|
Title: "Penalidade de Presença",
|
||||||
|
SubTitle:
|
||||||
|
"Um valor maior aumenta a probabilidade de falar sobre novos tópicos",
|
||||||
|
},
|
||||||
|
FrequencyPenalty: {
|
||||||
|
Title: "Penalidade de Frequência",
|
||||||
|
SubTitle:
|
||||||
|
"Um valor maior diminui a probabilidade de repetir a mesma linha",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Store: {
|
||||||
|
DefaultTopic: "Nova Conversa",
|
||||||
|
BotHello: "Olá! Como posso ajudá-lo hoje?",
|
||||||
|
Error: "Algo deu errado, por favor tente novamente mais tarde.",
|
||||||
|
Prompt: {
|
||||||
|
History: (content: string) =>
|
||||||
|
"Este é um resumo do histórico de chat como um recapitulativo: " +
|
||||||
|
content,
|
||||||
|
Topic:
|
||||||
|
"Por favor, gere um título de quatro a cinco palavras resumindo nossa conversa sem qualquer introdução, pontuação, aspas, períodos, símbolos ou texto adicional. Remova as aspas que o envolvem.",
|
||||||
|
Summarize:
|
||||||
|
"Resuma a discussão brevemente em 200 palavras ou menos para usar como um prompt para o contexto futuro.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Copy: {
|
||||||
|
Success: "Copiado para a área de transferência",
|
||||||
|
Failed:
|
||||||
|
"Falha na cópia, por favor conceda permissão para acessar a área de transferência",
|
||||||
|
},
|
||||||
|
Download: {
|
||||||
|
Success: "Conteúdo baixado para seu diretório.",
|
||||||
|
Failed: "Falha no download.",
|
||||||
|
},
|
||||||
|
Context: {
|
||||||
|
Toast: (x: any) => `Com ${x} prompts contextuais`,
|
||||||
|
Edit: "Configurações do Chat Atual",
|
||||||
|
Add: "Adicionar um Prompt",
|
||||||
|
Clear: "Contexto Limpo",
|
||||||
|
Revert: "Reverter",
|
||||||
|
},
|
||||||
|
Plugin: {
|
||||||
|
Name: "Plugin",
|
||||||
|
},
|
||||||
|
FineTuned: {
|
||||||
|
Sysmessage: "Você é um assistente que",
|
||||||
|
},
|
||||||
|
Mask: {
|
||||||
|
Name: "Máscara",
|
||||||
|
Page: {
|
||||||
|
Title: "Template de Prompt",
|
||||||
|
SubTitle: (count: number) => `${count} templates de prompt`,
|
||||||
|
Search: "Buscar Templates",
|
||||||
|
Create: "Criar",
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
Info: (count: number) => `${count} prompts`,
|
||||||
|
Chat: "Chat",
|
||||||
|
View: "Visualizar",
|
||||||
|
Edit: "Editar",
|
||||||
|
Delete: "Deletar",
|
||||||
|
DeleteConfirm: "Confirmar para deletar?",
|
||||||
|
},
|
||||||
|
EditModal: {
|
||||||
|
Title: (readonly: boolean) =>
|
||||||
|
`Editar Template de Prompt ${readonly ? "(somente leitura)" : ""}`,
|
||||||
|
Download: "Baixar",
|
||||||
|
Clone: "Clonar",
|
||||||
|
},
|
||||||
|
Config: {
|
||||||
|
Avatar: "Avatar do Bot",
|
||||||
|
Name: "Nome do Bot",
|
||||||
|
Sync: {
|
||||||
|
Title: "Usar Configuração Global",
|
||||||
|
SubTitle: "Usar configuração global neste chat",
|
||||||
|
Confirm:
|
||||||
|
"Confirmar para substituir a configuração personalizada pela configuração global?",
|
||||||
|
},
|
||||||
|
HideContext: {
|
||||||
|
Title: "Esconder Prompts de Contexto",
|
||||||
|
SubTitle: "Não mostrar prompts de contexto no chat",
|
||||||
|
},
|
||||||
|
Share: {
|
||||||
|
Title: "Compartilhar Esta Máscara",
|
||||||
|
SubTitle: "Gerar um link para esta máscara",
|
||||||
|
Action: "Copiar Link",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewChat: {
|
||||||
|
Return: "Retornar",
|
||||||
|
Skip: "Apenas Começar",
|
||||||
|
Title: "Escolher uma Máscara",
|
||||||
|
SubTitle: "Converse com a Alma por trás da Máscara",
|
||||||
|
More: "Encontre Mais",
|
||||||
|
NotShow: "Nunca Mostrar Novamente",
|
||||||
|
ConfirmNoShow:
|
||||||
|
"Confirmar para desabilitar?Você pode habilitar nas configurações depois.",
|
||||||
|
},
|
||||||
|
|
||||||
|
UI: {
|
||||||
|
Confirm: "Confirmar",
|
||||||
|
Cancel: "Cancelar",
|
||||||
|
Close: "Fechar",
|
||||||
|
Create: "Criar",
|
||||||
|
Edit: "Editar",
|
||||||
|
Export: "Exportar",
|
||||||
|
Import: "Importar",
|
||||||
|
Sync: "Sincronizar",
|
||||||
|
Config: "Configurar",
|
||||||
|
},
|
||||||
|
Exporter: {
|
||||||
|
Description: {
|
||||||
|
Title: "Apenas mensagens após a limpeza do contexto serão exibidas",
|
||||||
|
},
|
||||||
|
Model: "Modelo",
|
||||||
|
Messages: "Mensagens",
|
||||||
|
Topic: "Tópico",
|
||||||
|
Time: "Tempo",
|
||||||
|
},
|
||||||
|
|
||||||
|
URLCommand: {
|
||||||
|
Code: "Código de acesso detectado a partir da url, confirmar para aplicar? ",
|
||||||
|
Settings:
|
||||||
|
"Configurações detectadas a partir da url, confirmar para aplicar?",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default pt;
|
@@ -49,7 +49,7 @@ export const useAccessStore = createPersistStore(
|
|||||||
},
|
},
|
||||||
|
|
||||||
isValidOpenAI() {
|
isValidOpenAI() {
|
||||||
return ensure(get(), ["openaiUrl", "openaiApiKey"]);
|
return ensure(get(), ["openaiApiKey"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
isValidAzure() {
|
isValidAzure() {
|
||||||
|
@@ -557,7 +557,10 @@ export const useChatStore = createPersistStore(
|
|||||||
},
|
},
|
||||||
onFinish(message) {
|
onFinish(message) {
|
||||||
console.log("[Memory] ", message);
|
console.log("[Memory] ", message);
|
||||||
|
get().updateCurrentSession((session) => {
|
||||||
session.lastSummarizeIndex = lastSummarizeIndex;
|
session.lastSummarizeIndex = lastSummarizeIndex;
|
||||||
|
session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
console.error("[Summarize] ", err);
|
console.error("[Summarize] ", err);
|
||||||
|
@@ -3,7 +3,10 @@ import { showToast } from "./components/ui-lib";
|
|||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
|
|
||||||
export function trimTopic(topic: string) {
|
export function trimTopic(topic: string) {
|
||||||
return topic.replace(/[,。!?”“"、,.!?]*$/, "");
|
// Fix an issue where double quotes still show in the Indonesian language
|
||||||
|
// This will remove the specified punctuation from the end of the string
|
||||||
|
// and also trim quotes from both the start and end if they exist.
|
||||||
|
return topic.replace(/^["“”]+|["“”]+$/g, "").replace(/[,。!?”“"、,.!?]*$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyToClipboard(text: string) {
|
export async function copyToClipboard(text: string) {
|
||||||
|
@@ -8,7 +8,7 @@ export function useAllModels() {
|
|||||||
const models = useMemo(() => {
|
const models = useMemo(() => {
|
||||||
return collectModels(
|
return collectModels(
|
||||||
configStore.models,
|
configStore.models,
|
||||||
[accessStore.customModels, configStore.customModels].join(","),
|
[configStore.customModels, accessStore.customModels].join(","),
|
||||||
);
|
);
|
||||||
}, [accessStore.customModels, configStore.customModels, configStore.models]);
|
}, [accessStore.customModels, configStore.customModels, configStore.models]);
|
||||||
|
|
||||||
|
@@ -4,21 +4,40 @@ export function collectModelTable(
|
|||||||
models: readonly LLMModel[],
|
models: readonly LLMModel[],
|
||||||
customModels: string,
|
customModels: string,
|
||||||
) {
|
) {
|
||||||
const modelTable: Record<string, boolean> = {};
|
const modelTable: Record<
|
||||||
|
string,
|
||||||
|
{ available: boolean; name: string; displayName: string }
|
||||||
|
> = {};
|
||||||
|
|
||||||
// default models
|
// default models
|
||||||
models.forEach((m) => (modelTable[m.name] = m.available));
|
models.forEach(
|
||||||
|
(m) =>
|
||||||
|
(modelTable[m.name] = {
|
||||||
|
...m,
|
||||||
|
displayName: m.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// server custom models
|
// server custom models
|
||||||
customModels
|
customModels
|
||||||
.split(",")
|
.split(",")
|
||||||
.filter((v) => !!v && v.length > 0)
|
.filter((v) => !!v && v.length > 0)
|
||||||
.map((m) => {
|
.map((m) => {
|
||||||
if (m.startsWith("+")) {
|
const available = !m.startsWith("-");
|
||||||
modelTable[m.slice(1)] = true;
|
const nameConfig =
|
||||||
} else if (m.startsWith("-")) {
|
m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m;
|
||||||
modelTable[m.slice(1)] = false;
|
const [name, displayName] = nameConfig.split("=");
|
||||||
} else modelTable[m] = true;
|
|
||||||
|
// enable or disable all models
|
||||||
|
if (name === "all") {
|
||||||
|
Object.values(modelTable).forEach((m) => (m.available = available));
|
||||||
|
}
|
||||||
|
|
||||||
|
modelTable[name] = {
|
||||||
|
name,
|
||||||
|
displayName: displayName || name,
|
||||||
|
available,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
return modelTable;
|
return modelTable;
|
||||||
}
|
}
|
||||||
@@ -31,10 +50,7 @@ export function collectModels(
|
|||||||
customModels: string,
|
customModels: string,
|
||||||
) {
|
) {
|
||||||
const modelTable = collectModelTable(models, customModels);
|
const modelTable = collectModelTable(models, customModels);
|
||||||
const allModels = Object.keys(modelTable).map((m) => ({
|
const allModels = Object.values(modelTable);
|
||||||
name: m,
|
|
||||||
available: modelTable[m],
|
|
||||||
}));
|
|
||||||
|
|
||||||
return allModels;
|
return allModels;
|
||||||
}
|
}
|
||||||
|
10
package.json
10
package.json
@@ -20,11 +20,11 @@
|
|||||||
"@hello-pangea/dnd": "^16.3.0",
|
"@hello-pangea/dnd": "^16.3.0",
|
||||||
"@svgr/webpack": "^6.5.1",
|
"@svgr/webpack": "^6.5.1",
|
||||||
"@vercel/analytics": "^0.1.11",
|
"@vercel/analytics": "^0.1.11",
|
||||||
"emoji-picker-react": "^4.5.1",
|
"emoji-picker-react": "^4.5.15",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"mermaid": "^10.3.1",
|
"mermaid": "^10.6.1",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^5.0.3",
|
||||||
"next": "^13.4.9",
|
"next": "^13.4.9",
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -43,11 +43,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.4.0",
|
"@tauri-apps/cli": "^1.4.0",
|
||||||
"@types/node": "^20.3.3",
|
"@types/node": "^20.9.0",
|
||||||
"@types/react": "^18.2.14",
|
"@types/react": "^18.2.14",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-katex": "^3.0.0",
|
"@types/react-katex": "^3.0.0",
|
||||||
"@types/spark-md5": "^3.0.2",
|
"@types/spark-md5": "^3.0.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.49.0",
|
"eslint": "^8.49.0",
|
||||||
"eslint-config-next": "13.4.19",
|
"eslint-config-next": "13.4.19",
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "ChatGPT Next Web",
|
"productName": "ChatGPT Next Web",
|
||||||
"version": "2.9.11"
|
"version": "2.9.13"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
19
vercel.json
19
vercel.json
@@ -1,24 +1,5 @@
|
|||||||
{
|
{
|
||||||
"github": {
|
"github": {
|
||||||
"silent": true
|
"silent": true
|
||||||
},
|
|
||||||
"headers": [
|
|
||||||
{
|
|
||||||
"source": "/(.*)",
|
|
||||||
"headers": [
|
|
||||||
{
|
|
||||||
"key": "X-Real-IP",
|
|
||||||
"value": "$remote_addr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "X-Forwarded-For",
|
|
||||||
"value": "$proxy_add_x_forwarded_for"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Host",
|
|
||||||
"value": "$http_host"
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
54
yarn.lock
54
yarn.lock
@@ -1507,10 +1507,12 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
||||||
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
||||||
|
|
||||||
"@types/node@*", "@types/node@^20.3.3":
|
"@types/node@*", "@types/node@^20.9.0":
|
||||||
version "20.3.3"
|
version "20.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.0.tgz#bfcdc230583aeb891cf51e73cfdaacdd8deae298"
|
||||||
integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==
|
integrity sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
@@ -1550,10 +1552,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||||
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
||||||
|
|
||||||
"@types/spark-md5@^3.0.2":
|
"@types/spark-md5@^3.0.4":
|
||||||
version "3.0.2"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.2.tgz#da2e8a778a20335fc4f40b6471c4b0d86b70da55"
|
resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.4.tgz#c1221d63c069d95aba0c06a765b80661cacc12bf"
|
||||||
integrity sha512-82E/lVRaqelV9qmRzzJ1PKTpyrpnT7mwdneKNJB9hUtypZDMggloDfFUCIqRRx3lYRxteCwXSq9c+W71Vf0QnQ==
|
integrity sha512-qtOaDz+IXiNndPgYb6t1YoutnGvFRtWSNzpVjkAPCfB2UzTyybuD4Tjgs7VgRawum3JnJNRwNQd4N//SvrHg1Q==
|
||||||
|
|
||||||
"@types/unist@*", "@types/unist@^2.0.0":
|
"@types/unist@*", "@types/unist@^2.0.0":
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
@@ -2126,11 +2128,6 @@ client-only@0.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
|
|
||||||
clsx@^1.2.1:
|
|
||||||
version "1.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
|
||||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
|
||||||
|
|
||||||
color-convert@^1.9.0:
|
color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
@@ -2762,12 +2759,10 @@ elkjs@^0.8.2:
|
|||||||
resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
|
resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
|
||||||
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
|
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
|
||||||
|
|
||||||
emoji-picker-react@^4.5.1:
|
emoji-picker-react@^4.5.15:
|
||||||
version "4.5.1"
|
version "4.5.15"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.1.tgz#341f27dc86ad09340a316e0632484fcb9aff7195"
|
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.15.tgz#e12797c50584cb8af8aee7eb6c7c8fd953e41f7e"
|
||||||
integrity sha512-zpm0ui0TWkXZDUIevyNM0rC9Jyqc08RvVXH0KgsbSkDr+VgMQmYLu6UeI4SIWMZKsKMjQwujPpncRCFlEeykjw==
|
integrity sha512-BTqo+pNUE8kqX8BKFTbD4fhlxcA69qfie5En4PerReLaaPfXVyRlDJ1uf85nKj2u5esUQ999iUf8YyqcPsM2Qw==
|
||||||
dependencies:
|
|
||||||
clsx "^1.2.1"
|
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
@@ -4314,10 +4309,10 @@ merge2@^1.3.0, merge2@^1.4.1:
|
|||||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||||
|
|
||||||
mermaid@^10.3.1:
|
mermaid@^10.6.1:
|
||||||
version "10.3.1"
|
version "10.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.3.1.tgz#2f3c7e9f6bd7a8da2bef71cce2a542c8eba2a62e"
|
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.6.1.tgz#701f4160484137a417770ce757ce1887a98c00fc"
|
||||||
integrity sha512-hkenh7WkuRWPcob3oJtrN3W+yzrrIYuWF1OIfk/d0xGE8UWlvDhfexaHmDwwe8DKQgqMLI8DWEPwGprxkumjuw==
|
integrity sha512-Hky0/RpOw/1il9X8AvzOEChfJtVvmXm+y7JML5C//ePYMy0/9jCEmW1E1g86x9oDfW9+iVEdTV/i+M6KWRNs4A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@braintree/sanitize-url" "^6.0.1"
|
"@braintree/sanitize-url" "^6.0.1"
|
||||||
"@types/d3-scale" "^4.0.3"
|
"@types/d3-scale" "^4.0.3"
|
||||||
@@ -4690,10 +4685,10 @@ nanoid@^3.3.4:
|
|||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
||||||
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
||||||
|
|
||||||
nanoid@^4.0.2:
|
nanoid@^5.0.3:
|
||||||
version "4.0.2"
|
version "5.0.3"
|
||||||
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.3.tgz#6c97f53d793a7a1de6a38ebb46f50f95bf9793c7"
|
||||||
integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==
|
integrity sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==
|
||||||
|
|
||||||
natural-compare@^1.4.0:
|
natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
@@ -5799,6 +5794,11 @@ unbox-primitive@^1.0.2:
|
|||||||
has-symbols "^1.0.3"
|
has-symbols "^1.0.3"
|
||||||
which-boxed-primitive "^1.0.2"
|
which-boxed-primitive "^1.0.2"
|
||||||
|
|
||||||
|
undici-types@~5.26.4:
|
||||||
|
version "5.26.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||||
|
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@^2.0.0:
|
unicode-canonical-property-names-ecmascript@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
|
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
|
||||||
|
Reference in New Issue
Block a user