mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-01 12:46:58 +08:00
Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d226090926 | ||
|
2d534bfdf4 | ||
|
ed5cd11d6a | ||
|
0a60a87c9f | ||
|
506cdbc83c | ||
|
a64c4384b1 | ||
|
d54c983187 | ||
|
cd5f8f7407 | ||
|
00a282214e | ||
|
44145f11db | ||
|
ad63b10aea | ||
|
327ac765df | ||
|
83cea2adb8 | ||
|
0385f6ede9 | ||
|
45bf2c3d25 | ||
|
e6b64b0f2c | ||
|
4dc1e025e1 | ||
|
ba08b10de1 | ||
|
de35862cc5 | ||
|
407c9fc9c3 | ||
|
536358cb3c | ||
|
5f7a264e52 | ||
|
c70c311989 | ||
|
e5aa72af76 | ||
|
eb586ba361 | ||
|
7f16698f01 | ||
|
61eb356fd9 | ||
|
35a402c67e | ||
|
5a910e0f29 | ||
|
be8a35063c | ||
|
df75b9973a | ||
|
2f2e0b6762 |
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
images: yidadaa/chatgpt-next-web
|
images: yidadaa/chatgpt-next-web
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
type=semver,pattern={{version}}
|
type=ref,event=tag
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
|
29
.github/workflows/sync.yml
vendored
Normal file
29
.github/workflows/sync.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Upstream Sync
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */12 * * *' # every 12 hours
|
||||||
|
workflow_dispatch: # on button click
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync_latest_from_upstream:
|
||||||
|
name: Sync latest commits from upstream repo
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Step 1: run a standard checkout action, provided by github
|
||||||
|
- name: Checkout target repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Step 2: run the sync action
|
||||||
|
- name: Sync upstream changes
|
||||||
|
id: sync
|
||||||
|
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
||||||
|
with:
|
||||||
|
upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
|
||||||
|
upstream_sync_branch: main
|
||||||
|
target_sync_branch: main
|
||||||
|
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
||||||
|
|
||||||
|
# Set test_mode true to run tests instead of the true action!!
|
||||||
|
test_mode: false
|
16
README.md
16
README.md
@@ -36,11 +36,21 @@ One-Click to deploy your own ChatGPT web UI.
|
|||||||
- Automatically compresses chat history to support long conversations while also saving your tokens
|
- Automatically compresses chat history to support long conversations while also saving your tokens
|
||||||
- One-click export all chat history with full Markdown support
|
- One-click export all chat history with full Markdown support
|
||||||
|
|
||||||
## 使用
|
## 开发计划 Roadmap
|
||||||
|
- System Prompt: pin a user defined prompt as system prompt 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
||||||
|
- User Prompt: user can edit and save custom prompts to prompt list 允许用户自行编辑内置 Prompt 列表
|
||||||
|
- Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. 支持自部署的大语言模型
|
||||||
|
- Plugins: support network search, caculator, any other apis etc. 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||||
|
|
||||||
|
### 不会开发的功能 Not in Plan
|
||||||
|
- User login, accounts, cloud sync 用户登录、账号管理、消息云同步
|
||||||
|
- UI text customize 界面文字自定义
|
||||||
|
|
||||||
|
## 开始使用
|
||||||
|
|
||||||
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
|
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
|
||||||
2. 点击右侧按钮开始部署:
|
2. 点击右侧按钮开始部署:
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登陆即可,记得在环境变量页填入 API Key;
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key;
|
||||||
3. 部署完毕后,即可开始使用;
|
3. 部署完毕后,即可开始使用;
|
||||||
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
|
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
|
||||||
|
|
||||||
@@ -191,4 +201,4 @@ docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-
|
|||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
- [Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
|
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
|
||||||
|
@@ -3,8 +3,10 @@ import { requestOpenai } from "../common";
|
|||||||
|
|
||||||
async function makeRequest(req: NextRequest) {
|
async function makeRequest(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const res = await requestOpenai(req);
|
const api = await requestOpenai(req);
|
||||||
return new Response(res.body);
|
const res = new NextResponse(api.body);
|
||||||
|
res.headers.set("Content-Type", "application/json");
|
||||||
|
return res;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[OpenAI] ", req.body, e);
|
console.error("[OpenAI] ", req.body, e);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
@@ -218,7 +218,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 100px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-body-title {
|
.chat-body-title {
|
||||||
@@ -342,9 +341,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-panel {
|
.chat-input-panel {
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@@ -121,7 +121,7 @@ export function ChatList() {
|
|||||||
key={i}
|
key={i}
|
||||||
selected={i === selectedIndex}
|
selected={i === selectedIndex}
|
||||||
onClick={() => selectSession(i)}
|
onClick={() => selectSession(i)}
|
||||||
onDelete={() => removeSession(i)}
|
onDelete={() => confirm(Locale.Home.DeleteChat) && removeSession(i)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -132,9 +132,9 @@ function useSubmitHandler() {
|
|||||||
const config = useChatStore((state) => state.config);
|
const config = useChatStore((state) => state.config);
|
||||||
const submitKey = config.submitKey;
|
const submitKey = config.submitKey;
|
||||||
|
|
||||||
const shouldSubmit = (e: KeyboardEvent) => {
|
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key !== "Enter") return false;
|
if (e.key !== "Enter") return false;
|
||||||
|
if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
|
||||||
return (
|
return (
|
||||||
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
||||||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
||||||
@@ -256,7 +256,7 @@ export function Chat(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// check if should send message
|
// check if should send message
|
||||||
const onInputKeyDown = (e: KeyboardEvent) => {
|
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (shouldSubmit(e)) {
|
if (shouldSubmit(e)) {
|
||||||
onUserSubmit();
|
onUserSubmit();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -453,7 +453,10 @@ export function Chat(props: {
|
|||||||
className="markdown-body"
|
className="markdown-body"
|
||||||
style={{ fontSize: `${fontSize}px` }}
|
style={{ fontSize: `${fontSize}px` }}
|
||||||
onContextMenu={(e) => onRightClick(e, message)}
|
onContextMenu={(e) => onRightClick(e, message)}
|
||||||
onDoubleClickCapture={() => setUserInput(message.content)}
|
onDoubleClickCapture={() => {
|
||||||
|
if (!isMobileScreen()) return;
|
||||||
|
setUserInput(message.content);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Markdown content={message.content} />
|
<Markdown content={message.content} />
|
||||||
</div>
|
</div>
|
||||||
@@ -470,7 +473,7 @@ export function Chat(props: {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div ref={latestMessageRef} style={{ opacity: 0, height: "4em" }}>
|
<div ref={latestMessageRef} style={{ opacity: 0, height: "1px" }}>
|
||||||
-
|
-
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -485,7 +488,7 @@ export function Chat(props: {
|
|||||||
rows={4}
|
rows={4}
|
||||||
onInput={(e) => onInput(e.currentTarget.value)}
|
onInput={(e) => onInput(e.currentTarget.value)}
|
||||||
value={userInput}
|
value={userInput}
|
||||||
onKeyDown={(e) => onInputKeyDown(e as any)}
|
onKeyDown={onInputKeyDown}
|
||||||
onFocus={() => setAutoScroll(true)}
|
onFocus={() => setAutoScroll(true)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setAutoScroll(false);
|
setAutoScroll(false);
|
||||||
|
@@ -49,14 +49,14 @@ function SettingItem(props: {
|
|||||||
|
|
||||||
export function Settings(props: { closeSettings: () => void }) {
|
export function Settings(props: { closeSettings: () => void }) {
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const [config, updateConfig, resetConfig, clearAllData] = useChatStore(
|
const [config, updateConfig, resetConfig, clearAllData, clearSessions] =
|
||||||
(state) => [
|
useChatStore((state) => [
|
||||||
state.config,
|
state.config,
|
||||||
state.updateConfig,
|
state.updateConfig,
|
||||||
state.resetConfig,
|
state.resetConfig,
|
||||||
state.clearAllData,
|
state.clearAllData,
|
||||||
],
|
state.clearSessions,
|
||||||
);
|
]);
|
||||||
|
|
||||||
const updateStore = useUpdateStore();
|
const updateStore = useUpdateStore();
|
||||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||||
@@ -120,7 +120,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<ClearIcon />}
|
icon={<ClearIcon />}
|
||||||
onClick={clearAllData}
|
onClick={clearSessions}
|
||||||
bordered
|
bordered
|
||||||
title={Locale.Settings.Actions.ClearAll}
|
title={Locale.Settings.Actions.ClearAll}
|
||||||
/>
|
/>
|
||||||
|
@@ -8,7 +8,8 @@ import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
|
|||||||
let COMMIT_ID: string | undefined;
|
let COMMIT_ID: string | undefined;
|
||||||
try {
|
try {
|
||||||
COMMIT_ID = process
|
COMMIT_ID = process
|
||||||
.execSync("git describe --tags --abbrev=0")
|
// .execSync("git describe --tags --abbrev=0")
|
||||||
|
.execSync("git rev-parse --short HEAD")
|
||||||
.toString()
|
.toString()
|
||||||
.trim();
|
.trim();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@@ -57,6 +57,7 @@ const cn = {
|
|||||||
cn: "简体中文",
|
cn: "简体中文",
|
||||||
en: "English",
|
en: "English",
|
||||||
tw: "繁體中文",
|
tw: "繁體中文",
|
||||||
|
es: "Español",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "头像",
|
Avatar: "头像",
|
||||||
|
@@ -54,11 +54,12 @@ const en: LocaleType = {
|
|||||||
Close: "Close",
|
Close: "Close",
|
||||||
},
|
},
|
||||||
Lang: {
|
Lang: {
|
||||||
Name: "语言",
|
Name: "Language",
|
||||||
Options: {
|
Options: {
|
||||||
cn: "简体中文",
|
cn: "简体中文",
|
||||||
en: "English",
|
en: "English",
|
||||||
tw: "繁體中文",
|
tw: "繁體中文",
|
||||||
|
es: "Español",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "Avatar",
|
Avatar: "Avatar",
|
||||||
|
156
app/locales/es.ts
Normal file
156
app/locales/es.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { SubmitKey } from "../store/app";
|
||||||
|
import type { LocaleType } from "./index";
|
||||||
|
|
||||||
|
const es: LocaleType = {
|
||||||
|
WIP: "En construcción...",
|
||||||
|
Error: {
|
||||||
|
Unauthorized:
|
||||||
|
"Acceso no autorizado, por favor ingrese el código de acceso en la página de configuración.",
|
||||||
|
},
|
||||||
|
ChatItem: {
|
||||||
|
ChatItemCount: (count: number) => `${count} mensajes`,
|
||||||
|
},
|
||||||
|
Chat: {
|
||||||
|
SubTitle: (count: number) => `${count} mensajes con ChatGPT`,
|
||||||
|
Actions: {
|
||||||
|
ChatList: "Ir a la lista de chats",
|
||||||
|
CompressedHistory: "Historial de memoria comprimido",
|
||||||
|
Export: "Exportar todos los mensajes como Markdown",
|
||||||
|
Copy: "Copiar",
|
||||||
|
Stop: "Detener",
|
||||||
|
Retry: "Reintentar",
|
||||||
|
},
|
||||||
|
Rename: "Renombrar chat",
|
||||||
|
Typing: "Escribiendo...",
|
||||||
|
Input: (submitKey: string) => {
|
||||||
|
var inputHints = `Escribe algo y presiona ${submitKey} para enviar`;
|
||||||
|
if (submitKey === String(SubmitKey.Enter)) {
|
||||||
|
inputHints += ", presiona Shift + Enter para nueva línea";
|
||||||
|
}
|
||||||
|
return inputHints;
|
||||||
|
},
|
||||||
|
Send: "Enviar",
|
||||||
|
},
|
||||||
|
Export: {
|
||||||
|
Title: "Todos los mensajes",
|
||||||
|
Copy: "Copiar todo",
|
||||||
|
Download: "Descargar",
|
||||||
|
},
|
||||||
|
Memory: {
|
||||||
|
Title: "Historial de memoria",
|
||||||
|
EmptyContent: "Aún no hay nada.",
|
||||||
|
Copy: "Copiar todo",
|
||||||
|
},
|
||||||
|
Home: {
|
||||||
|
NewChat: "Nuevo chat",
|
||||||
|
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
|
||||||
|
},
|
||||||
|
Settings: {
|
||||||
|
Title: "Configuración",
|
||||||
|
SubTitle: "Todas las configuraciones",
|
||||||
|
Actions: {
|
||||||
|
ClearAll: "Borrar todos los datos",
|
||||||
|
ResetAll: "Restablecer todas las configuraciones",
|
||||||
|
Close: "Cerrar",
|
||||||
|
},
|
||||||
|
Lang: {
|
||||||
|
Name: "Language",
|
||||||
|
Options: {
|
||||||
|
cn: "简体中文",
|
||||||
|
en: "Inglés",
|
||||||
|
tw: "繁體中文",
|
||||||
|
es: "Español",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Avatar: "Avatar",
|
||||||
|
FontSize: {
|
||||||
|
Title: "Tamaño de fuente",
|
||||||
|
SubTitle: "Ajustar el tamaño de fuente del contenido del chat",
|
||||||
|
},
|
||||||
|
Update: {
|
||||||
|
Version: (x: string) => `Versión: ${x}`,
|
||||||
|
IsLatest: "Última versión",
|
||||||
|
CheckUpdate: "Buscar actualizaciones",
|
||||||
|
IsChecking: "Buscando actualizaciones...",
|
||||||
|
FoundUpdate: (x: string) => `Se encontró una nueva versión: ${x}`,
|
||||||
|
GoToUpdate: "Actualizar",
|
||||||
|
},
|
||||||
|
SendKey: "Tecla de envío",
|
||||||
|
Theme: "Tema",
|
||||||
|
TightBorder: "Borde ajustado",
|
||||||
|
Prompt: {
|
||||||
|
Disable: {
|
||||||
|
Title: "Desactivar autocompletado",
|
||||||
|
SubTitle: "Escribe / para activar el autocompletado",
|
||||||
|
},
|
||||||
|
List: "Lista de autocompletado",
|
||||||
|
ListCount: (builtin: number, custom: number) =>
|
||||||
|
`${builtin} incorporado, ${custom} definido por el usuario`,
|
||||||
|
Edit: "Editar",
|
||||||
|
},
|
||||||
|
HistoryCount: {
|
||||||
|
Title: "Cantidad de mensajes adjuntos",
|
||||||
|
SubTitle: "Número de mensajes enviados adjuntos por solicitud",
|
||||||
|
},
|
||||||
|
CompressThreshold: {
|
||||||
|
Title: "Umbral de compresión de historial",
|
||||||
|
SubTitle:
|
||||||
|
"Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor",
|
||||||
|
},
|
||||||
|
Token: {
|
||||||
|
Title: "Clave de API",
|
||||||
|
SubTitle: "Utiliza tu clave para ignorar el límite de código de acceso",
|
||||||
|
Placeholder: "Clave de la API de OpenAI",
|
||||||
|
},
|
||||||
|
Usage: {
|
||||||
|
Title: "Saldo de la cuenta",
|
||||||
|
SubTitle(granted: any, used: any) {
|
||||||
|
return `Total $${granted}, Usado $${used}`;
|
||||||
|
},
|
||||||
|
IsChecking: "Comprobando...",
|
||||||
|
Check: "Comprobar de nuevo",
|
||||||
|
},
|
||||||
|
AccessCode: {
|
||||||
|
Title: "Código de acceso",
|
||||||
|
SubTitle: "Control de acceso habilitado",
|
||||||
|
Placeholder: "Necesita código de acceso",
|
||||||
|
},
|
||||||
|
Model: "Modelo",
|
||||||
|
Temperature: {
|
||||||
|
Title: "Temperatura",
|
||||||
|
SubTitle: "Un valor mayor genera una salida más aleatoria",
|
||||||
|
},
|
||||||
|
MaxTokens: {
|
||||||
|
Title: "Máximo de tokens",
|
||||||
|
SubTitle: "Longitud máxima de tokens de entrada y tokens generados",
|
||||||
|
},
|
||||||
|
PresencePenlty: {
|
||||||
|
Title: "Penalización de presencia",
|
||||||
|
SubTitle:
|
||||||
|
"Un valor mayor aumenta la probabilidad de hablar sobre nuevos temas",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Store: {
|
||||||
|
DefaultTopic: "Nueva conversación",
|
||||||
|
BotHello: "¡Hola! ¿Cómo puedo ayudarte hoy?",
|
||||||
|
Error: "Algo salió mal, por favor intenta nuevamente más tarde.",
|
||||||
|
Prompt: {
|
||||||
|
History: (content: string) =>
|
||||||
|
"Este es un resumen del historial del chat entre la IA y el usuario como recapitulación: " +
|
||||||
|
content,
|
||||||
|
Topic:
|
||||||
|
"Por favor, genera un título de cuatro a cinco palabras que resuma nuestra conversación sin ningún inicio, puntuación, comillas, puntos, símbolos o texto adicional. Elimina las comillas que lo envuelven.",
|
||||||
|
Summarize:
|
||||||
|
"Resuma nuestra discusión brevemente en 50 caracteres o menos para usarlo como un recordatorio para futuros contextos.",
|
||||||
|
},
|
||||||
|
ConfirmClearAll:
|
||||||
|
"¿Confirmar para borrar todos los datos de chat y configuración?",
|
||||||
|
},
|
||||||
|
Copy: {
|
||||||
|
Success: "Copiado al portapapeles",
|
||||||
|
Failed:
|
||||||
|
"La copia falló, por favor concede permiso para acceder al portapapeles",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default es;
|
@@ -1,10 +1,11 @@
|
|||||||
import CN from "./cn";
|
import CN from "./cn";
|
||||||
import EN from "./en";
|
import EN from "./en";
|
||||||
import TW from "./tw";
|
import TW from "./tw";
|
||||||
|
import ES from "./es";
|
||||||
|
|
||||||
export type { LocaleType } from "./cn";
|
export type { LocaleType } from "./cn";
|
||||||
|
|
||||||
export const AllLangs = ["en", "cn", "tw"] as const;
|
export const AllLangs = ["en", "cn", "tw", "es"] as const;
|
||||||
type Lang = (typeof AllLangs)[number];
|
type Lang = (typeof AllLangs)[number];
|
||||||
|
|
||||||
const LANG_KEY = "lang";
|
const LANG_KEY = "lang";
|
||||||
@@ -44,6 +45,8 @@ export function getLang(): Lang {
|
|||||||
return "cn";
|
return "cn";
|
||||||
} else if (lang.includes("tw")) {
|
} else if (lang.includes("tw")) {
|
||||||
return "tw";
|
return "tw";
|
||||||
|
} else if (lang.includes("es")) {
|
||||||
|
return "es";
|
||||||
} else {
|
} else {
|
||||||
return "en";
|
return "en";
|
||||||
}
|
}
|
||||||
@@ -54,4 +57,4 @@ export function changeLang(lang: Lang) {
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { en: EN, cn: CN, tw: TW }[getLang()];
|
export default { en: EN, cn: CN, tw: TW, es: ES }[getLang()];
|
||||||
|
@@ -53,11 +53,12 @@ const tw: LocaleType = {
|
|||||||
Close: "關閉",
|
Close: "關閉",
|
||||||
},
|
},
|
||||||
Lang: {
|
Lang: {
|
||||||
Name: "語言",
|
Name: "Language",
|
||||||
Options: {
|
Options: {
|
||||||
cn: "简体中文",
|
cn: "简体中文",
|
||||||
en: "English",
|
en: "English",
|
||||||
tw: "繁體中文",
|
tw: "繁體中文",
|
||||||
|
es: "Español",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "大頭貼",
|
Avatar: "大頭貼",
|
||||||
|
@@ -2,6 +2,10 @@ import type { ChatRequest, ChatReponse } from "./api/openai/typing";
|
|||||||
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
|
|
||||||
|
if (!Array.prototype.at) {
|
||||||
|
require("array.prototype.at/auto");
|
||||||
|
}
|
||||||
|
|
||||||
const TIME_OUT_MS = 30000;
|
const TIME_OUT_MS = 30000;
|
||||||
|
|
||||||
const makeRequestParam = (
|
const makeRequestParam = (
|
||||||
@@ -69,10 +73,9 @@ export async function requestChat(messages: Message[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function requestUsage() {
|
export async function requestUsage() {
|
||||||
const res = await requestOpenaiClient("dashboard/billing/credit_grants")(
|
const res = await requestOpenaiClient(
|
||||||
null,
|
"dashboard/billing/credit_grants?_vercel_no_cache=1",
|
||||||
"GET",
|
)(null, "GET");
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = (await res.json()) as {
|
const response = (await res.json()) as {
|
||||||
|
@@ -11,6 +11,10 @@ import { trimTopic } from "../utils";
|
|||||||
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
|
||||||
|
if (!Array.prototype.at) {
|
||||||
|
require("array.prototype.at/auto");
|
||||||
|
}
|
||||||
|
|
||||||
export type Message = ChatCompletionResponseMessage & {
|
export type Message = ChatCompletionResponseMessage & {
|
||||||
date: string;
|
date: string;
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
@@ -185,6 +189,7 @@ interface ChatStore {
|
|||||||
config: ChatConfig;
|
config: ChatConfig;
|
||||||
sessions: ChatSession[];
|
sessions: ChatSession[];
|
||||||
currentSessionIndex: number;
|
currentSessionIndex: number;
|
||||||
|
clearSessions: () => void;
|
||||||
removeSession: (index: number) => void;
|
removeSession: (index: number) => void;
|
||||||
selectSession: (index: number) => void;
|
selectSession: (index: number) => void;
|
||||||
newSession: () => void;
|
newSession: () => void;
|
||||||
@@ -223,6 +228,13 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearSessions() {
|
||||||
|
set(() => ({
|
||||||
|
sessions: [createEmptySession()],
|
||||||
|
currentSessionIndex: 0,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
resetConfig() {
|
resetConfig() {
|
||||||
set(() => ({ config: { ...DEFAULT_CONFIG } }));
|
set(() => ({ config: { ...DEFAULT_CONFIG } }));
|
||||||
},
|
},
|
||||||
|
@@ -99,19 +99,19 @@ export const usePromptStore = create<PromptStore>()(
|
|||||||
({
|
({
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
} as Prompt)
|
} as Prompt),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.concat([...(state?.prompts?.values() ?? [])]);
|
.concat([...(state?.prompts?.values() ?? [])]);
|
||||||
|
|
||||||
const allPromptsForSearch = builtinPrompts.reduce(
|
const allPromptsForSearch = builtinPrompts.reduce(
|
||||||
(pre, cur) => pre.concat(cur),
|
(pre, cur) => pre.concat(cur),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
SearchService.count.builtin = res.en.length + res.cn.length;
|
SearchService.count.builtin = res.en.length + res.cn.length;
|
||||||
SearchService.init(allPromptsForSearch);
|
SearchService.init(allPromptsForSearch);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
@@ -26,8 +26,10 @@ export const useUpdateStore = create<UpdateStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await (await fetch(FETCH_TAG_URL)).json();
|
// const data = await (await fetch(FETCH_TAG_URL)).json();
|
||||||
const remoteId = data[0].name as string;
|
// const remoteId = data[0].name as string;
|
||||||
|
const data = await (await fetch(FETCH_COMMIT_URL)).json();
|
||||||
|
const remoteId = (data[0].sha as string).substring(0, 7);
|
||||||
set(() => ({
|
set(() => ({
|
||||||
lastUpdate: Date.now(),
|
lastUpdate: Date.now(),
|
||||||
remoteId,
|
remoteId,
|
||||||
|
@@ -53,7 +53,7 @@
|
|||||||
--sidebar-width: 300px;
|
--sidebar-width: 300px;
|
||||||
--window-content-width: calc(100% - var(--sidebar-width));
|
--window-content-width: calc(100% - var(--sidebar-width));
|
||||||
--message-max-width: 80%;
|
--message-max-width: 80%;
|
||||||
--full-height: 100vh;
|
--full-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
@@ -75,6 +75,9 @@
|
|||||||
@include dark;
|
@include dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
html {
|
||||||
|
height: var(--full-height);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--gray);
|
background-color: var(--gray);
|
||||||
|
10
app/utils.ts
10
app/utils.ts
@@ -2,15 +2,7 @@ import { showToast } from "./components/ui-lib";
|
|||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
|
|
||||||
export function trimTopic(topic: string) {
|
export function trimTopic(topic: string) {
|
||||||
const s = topic.split("");
|
return topic.replace(/[,。!?、,.!?]*$/, "");
|
||||||
let lastChar = s.at(-1); // 获取 s 的最后一个字符
|
|
||||||
let pattern = /[,。!?、,.!?]/; // 定义匹配中文和英文标点符号的正则表达式
|
|
||||||
while (lastChar && pattern.test(lastChar!)) {
|
|
||||||
s.pop();
|
|
||||||
lastChar = s.at(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyToClipboard(text: string) {
|
export function copyToClipboard(text: string) {
|
||||||
|
@@ -23,9 +23,9 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.5",
|
||||||
"remark-breaks": "^3.0.2",
|
|
||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"rehype-prism-plus": "^1.5.1",
|
"rehype-prism-plus": "^1.5.1",
|
||||||
|
"remark-breaks": "^3.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"sass": "^1.59.2",
|
"sass": "^1.59.2",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/react-katex": "^3.0.0",
|
"@types/react-katex": "^3.0.0",
|
||||||
"@types/spark-md5": "^3.0.2",
|
"@types/spark-md5": "^3.0.2",
|
||||||
|
"array.prototype.at": "^1.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-config-next": "13.2.3",
|
"eslint-config-next": "13.2.3",
|
||||||
|
10
yarn.lock
10
yarn.lock
@@ -1570,6 +1570,16 @@ array-union@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
||||||
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
||||||
|
|
||||||
|
array.prototype.at@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/array.prototype.at/-/array.prototype.at-1.1.1.tgz#6deda3cd3c704afa16361387ea344e0b8d8831b5"
|
||||||
|
integrity sha512-n/wYNLJy/fVEU9EGPt2ww920hy1XX3XB2yTREFy1QsxctBgQV/tZIwg1G8jVxELna4pLCzg/xvvS/DDXtI4NNg==
|
||||||
|
dependencies:
|
||||||
|
call-bind "^1.0.2"
|
||||||
|
define-properties "^1.1.4"
|
||||||
|
es-abstract "^1.20.4"
|
||||||
|
es-shim-unscopables "^1.0.0"
|
||||||
|
|
||||||
array.prototype.flat@^1.3.1:
|
array.prototype.flat@^1.3.1:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2"
|
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2"
|
||||||
|
Reference in New Issue
Block a user