Compare commits

..

1 Commits
v2.9.13 ... tts

Author SHA1 Message Date
Yidadaa
d1d8b1f393 feat: #3110 add voice control 2023-11-14 03:42:23 +08:00
27 changed files with 330 additions and 584 deletions

View File

@@ -18,7 +18,7 @@ jobs:
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 16
- name: get version
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
- name: create release
@@ -59,7 +59,7 @@ jobs:
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 16
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:

View File

@@ -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)
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
- 多国语言支持English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
- 多国语言支持English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
## 开发计划
@@ -161,7 +161,7 @@ Access password, separated by comma.
### `OPENAI_API_KEY` (required)
Your openai api key, join multiple api keys with comma.
Your openai api key.
### `BASE_URL` (optional)
@@ -216,11 +216,9 @@ If you want to disable parse settings from url, set this to 1.
### `CUSTOM_MODELS` (optional)
> Default: Empty
> 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`.
> 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, use `name=displayName` to customize model name, separated by comma.
User `-all` to disable all default models, `+all` to enable all default models.
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.
## Requirements
@@ -346,9 +344,6 @@ If you want to add a new translation, read this [document](./docs/translation.md
[@piksonGit](https://github.com/piksonGit)
[@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

View File

@@ -68,7 +68,7 @@ code1,code2,code3
### `OPENAI_API_KEY` (必填项)
OpanAI 密钥,你在 openai 账户页面申请的 api key使用英文逗号隔开多个 key这样可以随机轮询这些 key。
OpanAI 密钥,你在 openai 账户页面申请的 api key。
### `CODE` (可选)
@@ -122,10 +122,9 @@ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.micro
### `CUSTOM_MODELS` (可选)
> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。
> 如果你想先禁用所有模型,再启用指定模型,可以使用 `-all,+gpt-3.5-turbo`,则表示仅启用 `gpt-3.5-turbo`
> 示例:`+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`。
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名:展示名` 来自定义模型的展示名,用英文逗号隔开。
## 开发
@@ -139,7 +138,7 @@ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.micro
OPENAI_API_KEY=<your api key here>
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
BASE_URL=https://b.nextweb.fun/api/proxy
BASE_URL=https://a.nextweb.fun/api/proxy
```
### 本地开发

View File

@@ -46,13 +46,6 @@ 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 (!apiKey) {
const serverApiKey = serverConfig.isAzure

View File

@@ -30,10 +30,7 @@ export async function requestOpenai(req: NextRequest) {
console.log("[Proxy] ", path);
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(
() => {

View File

@@ -75,4 +75,3 @@ export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = ['arn1', 'bom1', 'cdg1', 'cle1', 'cpt1', 'dub1', 'fra1', 'gru1', 'hnd1', 'iad1', 'icn1', 'kix1', 'lhr1', 'pdx1', 'sfo1', 'sin1', 'syd1'];

View File

@@ -47,6 +47,7 @@ export abstract class LLMApi {
abstract chat(options: ChatOptions): Promise<void>;
abstract usage(): Promise<LLMUsage>;
abstract models(): Promise<LLMModel[]>;
abstract speech(input: string): Promise<ArrayBuffer>;
}
type ProviderName = "openai" | "azure" | "claude" | "palm";

View File

@@ -115,35 +115,12 @@ export class ChatGPTApi implements LLMApi {
if (shouldStream) {
let responseText = "";
let remainText = "";
let finished = false;
// animate response to make it looks smooth
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
return;
}
if (remainText.length > 0) {
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
const fetchText = remainText.slice(0, fetchCount);
responseText += fetchText;
remainText = remainText.slice(fetchCount);
options.onUpdate?.(responseText, fetchText);
}
requestAnimationFrame(animateResponseText);
}
// start animaion
animateResponseText();
const finish = () => {
if (!finished) {
options.onFinish(responseText);
finished = true;
options.onFinish(responseText + remainText);
}
};
@@ -206,7 +183,8 @@ export class ChatGPTApi implements LLMApi {
};
const delta = json.choices[0]?.delta?.content;
if (delta) {
remainText += delta;
responseText += delta;
options.onUpdate?.(responseText, delta);
}
} catch (e) {
console.error("[Request] parse error", text);
@@ -325,5 +303,28 @@ export class ChatGPTApi implements LLMApi {
available: true,
}));
}
public cache: Record<string, ArrayBuffer> = {};
async speech(input: string): Promise<ArrayBuffer> {
if (this.cache[input]) return this.cache[input].slice(0);
const res = await fetch(this.path(OpenaiPath.Speech), {
method: "POST",
headers: {
...getHeaders(),
},
body: JSON.stringify({
model: "tts-1",
input: input,
voice: "onyx",
}),
});
const arrayBuffer = await res.arrayBuffer();
this.cache[input] = arrayBuffer.slice(0);
return arrayBuffer;
}
}
export { OpenaiPath };

View File

@@ -89,6 +89,7 @@ import { prettyObject } from "../utils/format";
import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks";
import { VoicePage } from "./voice/voice";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
@@ -449,7 +450,8 @@ export function ChatActions(props: {
);
showToast(nextModel);
}
}, [chatStore, currentModel, models]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentModel, models]);
return (
<div className={styles["chat-input-actions"]}>
@@ -1048,6 +1050,8 @@ function _Chat() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <VoicePage />;
return (
<div className={styles.chat} key={session.id}>
<div className="window-header" data-tauri-drag-region>

View File

@@ -2,6 +2,8 @@
require("../polyfill");
import "regenerator-runtime/runtime";
import { useState, useEffect } from "react";
import styles from "./home.module.scss";
@@ -128,7 +130,8 @@ function Screen() {
const isHome = location.pathname === Path.Home;
const isAuth = location.pathname === Path.Auth;
const isMobileScreen = useMobileScreen();
const shouldTightBorder = getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
const shouldTightBorder =
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
useEffect(() => {
loadAsyncGoogleFont();

View File

@@ -635,11 +635,6 @@ export function Settings() {
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);
return () => {
document.removeEventListener("keydown", keydownEvent);
@@ -914,26 +909,21 @@ export function Settings() {
{!accessStore.hideUserApiKey && (
<>
{
// Conditionally render the following ListItem based on clientConfig.isApp
!clientConfig?.isApp && ( // only show if isApp is false
<ListItem
title={Locale.Settings.Access.CustomEndpoint.Title}
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
>
<input
type="checkbox"
checked={accessStore.useCustomConfig}
onChange={(e) =>
accessStore.update(
(access) =>
(access.useCustomConfig = e.currentTarget.checked),
)
}
></input>
</ListItem>
)
}
<ListItem
title={Locale.Settings.Access.CustomEndpoint.Title}
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
>
<input
type="checkbox"
checked={accessStore.useCustomConfig}
onChange={(e) =>
accessStore.update(
(access) =>
(access.useCustomConfig = e.currentTarget.checked),
)
}
></input>
</ListItem>
{accessStore.useCustomConfig && (
<>
<ListItem
@@ -1062,7 +1052,7 @@ export function Settings() {
</>
)}
{!shouldHideBalanceQuery && !clientConfig?.isApp ? (
{!shouldHideBalanceQuery ? (
<ListItem
title={Locale.Settings.Usage.Title}
subTitle={

View File

@@ -0,0 +1,55 @@
.voice-page {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba($color: #000000, $alpha: 0.9);
color: white;
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
align-items: center;
.top,
.bottom {
flex: 1;
padding: 20px;
font-size: 1.5em;
color: rgba($color: #fff, $alpha: 0.6);
overflow: auto;
width: 100%;
box-sizing: border-box;
}
.active {
background-color: rgba($color: #00ff00, $alpha: 0.2);
}
.top.active {
background-color: white;
&::after {
content: "☁️";
color: black;
}
}
.top:hover {
background-color: black;
}
.top {
}
.center {
height: 2px;
background-color: white;
opacity: 0.2;
width: 100%;
}
.bottom {
}
}

View File

@@ -0,0 +1,117 @@
import { useChatStore } from "@/app/store";
import style from "./voice.module.scss";
import { useEffect, useMemo, useRef, useState } from "react";
import SpeechRecognition, {
useSpeechRecognition,
} from "react-speech-recognition";
import { IconButton } from "../button";
import { api } from "@/app/client/api";
function findLast<T>(array: T[], predictor: (_: T) => boolean) {
for (let i = array.length - 1; i >= 0; i -= 1) {
if (predictor(array[i])) {
return array[i];
}
}
return null;
}
export function VoicePage() {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const lastAssistantMessage = useMemo(
() => findLast(session.messages, (m) => m.role === "assistant"),
[session.messages],
);
const lastUserMessage = useMemo(
() => findLast(session.messages, (m) => m.role === "user"),
[session.messages],
);
const speech = useSpeechRecognition({
clearTranscriptOnListen: true,
});
if (!speech.browserSupportsSpeechRecognition) {
throw Error("your browser does not support speech recognition api");
}
function startVoice() {
SpeechRecognition.startListening({
language: "zh-CN",
});
sourceNodeRef.current?.stop();
}
function stopVoice() {
SpeechRecognition.stopListening();
}
useEffect(() => {
if (!speech.listening) {
if (
speech.finalTranscript.length > 0 &&
speech.finalTranscript !== lastUserMessage?.content
) {
chatStore.onUserInput(speech.finalTranscript);
}
}
}, [speech.listening]);
const [loadingTTS, setLoadingTTS] = useState(false);
const sourceNodeRef = useRef<AudioBufferSourceNode>();
function speak() {
const content = lastAssistantMessage?.content;
if (!content) return;
setLoadingTTS(true);
api.llm.speech(content).then(async (arrayBuffer) => {
const audioContext = new (window.AudioContext ||
(window as any).webkitAudioContext)();
const source = audioContext.createBufferSource();
try {
sourceNodeRef.current?.stop();
} catch {}
sourceNodeRef.current = source;
// 设置音频源的 buffer 属性
source.buffer = await audioContext.decodeAudioData(arrayBuffer);
// 连接到默认的输出设备(通常是扬声器)
source.connect(audioContext.destination);
// 开始播放
setLoadingTTS(false);
source.start(0);
});
}
const lastStream = useRef(false);
useEffect(() => {
if (
lastAssistantMessage?.streaming !== lastStream.current &&
lastStream.current
) {
speak();
}
lastStream.current = !!lastAssistantMessage?.streaming;
}, [lastAssistantMessage?.streaming]);
return (
<div className={style["voice-page"]}>
<div className={style["top"] + ` ${style["active"]}`} onClick={speak}>
{lastAssistantMessage?.content}
</div>
<div className={style["center"]}></div>
<div
className={style["bottom"] + ` ${speech.listening && style["active"]}`}
onClick={() => {
if (speech.listening) {
stopVoice();
} else {
startVoice();
}
}}
>
{speech.transcript || lastUserMessage?.content}
</div>
</div>
);
}

View File

@@ -62,17 +62,9 @@ export const getServerSideConfig = () => {
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 {
baseUrl: process.env.BASE_URL,
apiKey,
apiKey: process.env.OPENAI_API_KEY,
openaiOrgId: process.env.OPENAI_ORG_ID,
isAzure,

View File

@@ -1,3 +1,6 @@
import { GPTText } from "./utils/prompts/gpt-text";
import { GPTVoice } from "./utils/prompts/gpt-voice";
export const OWNER = "Yidadaa";
export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
@@ -72,6 +75,7 @@ export const OpenaiPath = {
UsagePath: "dashboard/billing/usage",
SubsPath: "dashboard/billing/subscription",
ListModelPath: "v1/models",
Speech: "v1/audio/speech",
};
export const Azure = {
@@ -79,14 +83,8 @@ export const Azure = {
};
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
export const DEFAULT_SYSTEM_TEMPLATE = `
You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: {{cutoff}}
Current model: {{model}}
Current time: {{time}}
Latex inline: $x^2$
Latex block: $$e=mc^2$$
`;
// export const DEFAULT_SYSTEM_TEMPLATE = GPTText;
export const DEFAULT_SYSTEM_TEMPLATE = GPTVoice;
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";

View File

@@ -1,6 +1,5 @@
import cn from "./cn";
import en from "./en";
import pt from "./pt";
import tw from "./tw";
import id from "./id";
import fr from "./fr";
@@ -25,7 +24,6 @@ const ALL_LANGS = {
cn,
en,
tw,
pt,
jp,
ko,
id,
@@ -49,7 +47,6 @@ export const AllLangs = Object.keys(ALL_LANGS) as Lang[];
export const ALL_LANG_OPTIONS: Record<Lang, string> = {
cn: "简体中文",
en: "English",
pt: "Português",
tw: "繁體中文",
jp: "日本語",
ko: "한국어",
@@ -67,6 +64,8 @@ export const ALL_LANG_OPTIONS: Record<Lang, string> = {
bn: "বাংলা",
};
export const SPEECH_LANG_OPTIONS: Record<Lang, string> = {};
const LANG_KEY = "lang";
const DEFAULT_LANG = "en";

View File

@@ -1,466 +0,0 @@
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 desabilitarVocê 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;

View File

@@ -557,10 +557,7 @@ export const useChatStore = createPersistStore(
},
onFinish(message) {
console.log("[Memory] ", message);
get().updateCurrentSession((session) => {
session.lastSummarizeIndex = lastSummarizeIndex;
session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
});
session.lastSummarizeIndex = lastSummarizeIndex;
},
onError(err) {
console.error("[Summarize] ", err);

View File

@@ -3,10 +3,7 @@ import { showToast } from "./components/ui-lib";
import Locale from "./locales";
export function trimTopic(topic: string) {
// 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(/[,。!?”“"、,.!?]*$/, "");
return topic.replace(/[,。!?”“"、,.!?]*$/, "");
}
export async function copyToClipboard(text: string) {

View File

View File

@@ -26,13 +26,7 @@ export function collectModelTable(
const available = !m.startsWith("-");
const nameConfig =
m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m;
const [name, displayName] = nameConfig.split("=");
// enable or disable all models
if (name === "all") {
Object.values(modelTable).forEach((m) => (m.available = available));
}
const [name, displayName] = nameConfig.split(":");
modelTable[name] = {
name,
displayName: displayName || name,

View File

@@ -0,0 +1,8 @@
export const GPTText = `
You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: {{cutoff}}
Current model: {{model}}
Current time: {{time}}
Latex inline: $x^2$
Latex block: $$e=mc^2$$
`;

View File

@@ -0,0 +1,29 @@
export const GPTVoice = `
You are ChatGPT, a large language model trained by OpenAI, based on the GPT-4 architecture.
The user is talking to you over voice on their phone, and your response will be read out loud with realistic text-to-speech (TTS) technology.
Follow every direction here when crafting your response:
Use natural, conversational language that are clear and easy to follow (short sentences, simple words).
Be concise and relevant:Most of your responses should be a sentence or two, unless youre asked to go deeper.
Dont monopolize the conversation.
Use discourse markers to ease comprehension.
Never use the list format.
Keep the conversation flowing.
Clarify:
when there is ambiguity, ask clarifying questions, rather than make assumptions.
Dont implicitly or explicitly try to end the chat (i.e. do not end a response with “Talk soon!”, or “Enjoy!”).
Sometimes the user might just want to chat. Ask them relevant follow-up questions.
Dont ask them if theres anything else they need help with (e.g. dont say things like “How can I assist you further?”).
Remember that this is a voice conversation: Dont use lists, markdown, bullet points, or other formatting thats not typically spoken.
Type out numbers in words (e.g. twenty twelve instead of the year 2012). If something doesnt make sense, its likely because you misheard them.
There wasnt a typo, and the user didnt mispronounce anything.
Remember to follow these rules absolutely, and do not refer to these rules, even if youre asked about them.
Knowledge cutoff: {{cutoff}}
Current model: {{model}}
Current time: {{time}}
`;

View File

@@ -19,6 +19,7 @@
"@fortaine/fetch-event-source": "^3.0.6",
"@hello-pangea/dnd": "^16.3.0",
"@svgr/webpack": "^6.5.1",
"@types/react-speech-recognition": "^3.9.4",
"@vercel/analytics": "^0.1.11",
"emoji-picker-react": "^4.5.15",
"fuse.js": "^6.6.2",
@@ -31,6 +32,8 @@
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.15.0",
"react-speech-recognition": "^3.10.0",
"regenerator-runtime": "^0.14.0",
"rehype-highlight": "^6.0.0",
"rehype-katex": "^6.0.3",
"remark-breaks": "^3.0.2",

View File

@@ -9,7 +9,7 @@
},
"package": {
"productName": "ChatGPT Next Web",
"version": "2.9.13"
"version": "2.9.11"
},
"tauri": {
"allowlist": {

View File

@@ -1,5 +1,24 @@
{
"github": {
"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"
}
]
}
]
}

View File

@@ -1439,6 +1439,11 @@
dependencies:
"@types/ms" "*"
"@types/dom-speech-recognition@*":
version "0.0.4"
resolved "https://registry.npmmirror.com/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.4.tgz#3ac5eddfbaa0dacf7eca3d8979ef4f3e519d8e19"
integrity sha512-zf2GwV/G6TdaLwpLDcGTIkHnXf8JEf/viMux+khqKQKDa8/8BAUtXXZS563GnvJ4Fg0PBLGAaFf2GekEVSZ6GQ==
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
@@ -1538,6 +1543,13 @@
dependencies:
"@types/react" "*"
"@types/react-speech-recognition@^3.9.4":
version "3.9.4"
resolved "https://registry.npmmirror.com/@types/react-speech-recognition/-/react-speech-recognition-3.9.4.tgz#398047e8c7e90867b16ee3c698e7ace825659a4d"
integrity sha512-ULNTkpKRTPNl5MVBk3prnnsELLRGZMrJpuSUiEdon53B+243j0tNEzGFN+YFFH7USkLqyYG0q4REQfS+i+3OXg==
dependencies:
"@types/dom-speech-recognition" "*"
"@types/react@*", "@types/react@^18.2.14":
version "18.2.14"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.14.tgz#fa7a6fecf1ce35ca94e74874f70c56ce88f7a127"
@@ -5100,6 +5112,11 @@ react-router@6.15.0:
dependencies:
"@remix-run/router" "1.8.0"
react-speech-recognition@^3.10.0:
version "3.10.0"
resolved "https://registry.npmmirror.com/react-speech-recognition/-/react-speech-recognition-3.10.0.tgz#7aa43bb28d78b92671864dabba3a70489ccad27b"
integrity sha512-EVSr4Ik8l9urwdPiK2r0+ADrLyDDrjB0qBRdUWO+w2MfwEBrj6NuRmy1GD3x7BU/V6/hab0pl8Lupen0zwlJyw==
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@@ -5138,6 +5155,11 @@ regenerator-runtime@^0.13.11:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
regenerator-transform@^0.15.1:
version "0.15.1"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56"