mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-09-30 23:26:39 +08:00
Merge branch 'Yidadaa:main' into azure
This commit is contained in:
commit
2ab569df06
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
images: yidadaa/chatgpt-next-web
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{version}}
|
||||
type=ref,event=tag
|
||||
|
||||
-
|
||||
name: Set up QEMU
|
||||
@ -43,10 +43,10 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
|
||||
|
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
|
@ -6,13 +6,9 @@ RUN apk add --no-cache libc6-compat
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock* package-lock.json* ./
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn install --frozen-lockfile --network-timeout 100000; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
RUN yarn install
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
|
14
README.md
14
README.md
@ -36,7 +36,17 @@ One-Click to deploy your own ChatGPT web UI.
|
||||
- Automatically compresses chat history to support long conversations while also saving your tokens
|
||||
- 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);
|
||||
2. 点击右侧按钮开始部署:
|
||||
@ -191,4 +201,4 @@ docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-
|
||||
|
||||
## 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) {
|
||||
try {
|
||||
const res = await requestOpenai(req);
|
||||
return new Response(res.body);
|
||||
const api = await requestOpenai(req);
|
||||
const res = new NextResponse(api.body);
|
||||
res.headers.set('Content-Type', 'application/json');
|
||||
return res;
|
||||
} catch (e) {
|
||||
console.error("[OpenAI] ", req.body, e);
|
||||
return NextResponse.json(
|
||||
|
@ -26,13 +26,13 @@
|
||||
@media only screen and (min-width: 600px) {
|
||||
.tight-container {
|
||||
--window-width: 100vw;
|
||||
--window-height: 100vh;
|
||||
--window-height: var(--full-height);
|
||||
--window-content-width: calc(100% - var(--sidebar-width));
|
||||
|
||||
@include container();
|
||||
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
max-height: var(--full-height);
|
||||
|
||||
border-radius: 0;
|
||||
}
|
||||
@ -74,7 +74,7 @@
|
||||
position: absolute;
|
||||
left: -100%;
|
||||
z-index: 999;
|
||||
height: 100vh;
|
||||
height: var(--full-height);
|
||||
transition: all ease 0.3s;
|
||||
box-shadow: none;
|
||||
}
|
||||
@ -218,7 +218,6 @@
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.chat-body-title {
|
||||
@ -342,9 +341,6 @@
|
||||
}
|
||||
|
||||
.chat-input-panel {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
|
@ -23,7 +23,13 @@ import DownloadIcon from "../icons/download.svg";
|
||||
|
||||
import { Message, SubmitKey, useChatStore, ChatSession } from "../store";
|
||||
import { showModal, showToast } from "./ui-lib";
|
||||
import { copyToClipboard, downloadAs, isIOS, selectOrCopy } from "../utils";
|
||||
import {
|
||||
copyToClipboard,
|
||||
downloadAs,
|
||||
isIOS,
|
||||
isMobileScreen,
|
||||
selectOrCopy,
|
||||
} from "../utils";
|
||||
import Locale from "../locales";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
@ -115,7 +121,7 @@ export function ChatList() {
|
||||
key={i}
|
||||
selected={i === selectedIndex}
|
||||
onClick={() => selectSession(i)}
|
||||
onDelete={() => removeSession(i)}
|
||||
onDelete={() => confirm(Locale.Home.DeleteChat) && removeSession(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -126,9 +132,9 @@ function useSubmitHandler() {
|
||||
const config = useChatStore((state) => state.config);
|
||||
const submitKey = config.submitKey;
|
||||
|
||||
const shouldSubmit = (e: KeyboardEvent) => {
|
||||
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key !== "Enter") return false;
|
||||
|
||||
if(e.key==='Enter' && e.nativeEvent.isComposing) return false
|
||||
return (
|
||||
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
||||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
||||
@ -250,7 +256,7 @@ export function Chat(props: {
|
||||
};
|
||||
|
||||
// check if should send message
|
||||
const onInputKeyDown = (e: KeyboardEvent) => {
|
||||
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (shouldSubmit(e)) {
|
||||
onUserSubmit();
|
||||
e.preventDefault();
|
||||
@ -284,9 +290,7 @@ export function Chat(props: {
|
||||
|
||||
// for auto-scroll
|
||||
const latestMessageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// wont scroll while hovering messages
|
||||
const [autoScroll, setAutoScroll] = useState(false);
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
|
||||
// preview messages
|
||||
const messages = (session.messages as RenderMessage[])
|
||||
@ -319,7 +323,17 @@ export function Chat(props: {
|
||||
useLayoutEffect(() => {
|
||||
setTimeout(() => {
|
||||
const dom = latestMessageRef.current;
|
||||
if (dom && !isIOS() && autoScroll) {
|
||||
const inputDom = inputRef.current;
|
||||
|
||||
// only scroll when input overlaped message body
|
||||
let shouldScroll = true;
|
||||
if (dom && inputDom) {
|
||||
const domRect = dom.getBoundingClientRect();
|
||||
const inputRect = inputDom.getBoundingClientRect();
|
||||
shouldScroll = domRect.top > inputRect.top;
|
||||
}
|
||||
|
||||
if (dom && autoScroll && shouldScroll) {
|
||||
dom.scrollIntoView({
|
||||
block: "end",
|
||||
});
|
||||
@ -439,7 +453,10 @@ export function Chat(props: {
|
||||
className="markdown-body"
|
||||
style={{ fontSize: `${fontSize}px` }}
|
||||
onContextMenu={(e) => onRightClick(e, message)}
|
||||
onDoubleClickCapture={() => setUserInput(message.content)}
|
||||
onDoubleClickCapture={() => {
|
||||
if (!isMobileScreen()) return;
|
||||
setUserInput(message.content);
|
||||
}}
|
||||
>
|
||||
<Markdown content={message.content} />
|
||||
</div>
|
||||
@ -456,7 +473,7 @@ export function Chat(props: {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div ref={latestMessageRef} style={{ opacity: 0, height: "4em" }}>
|
||||
<div ref={latestMessageRef} style={{ opacity: 0, height: "1px" }}>
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
@ -471,11 +488,11 @@ export function Chat(props: {
|
||||
rows={4}
|
||||
onInput={(e) => onInput(e.currentTarget.value)}
|
||||
value={userInput}
|
||||
onKeyDown={(e) => onInputKeyDown(e as any)}
|
||||
onKeyDown={onInputKeyDown}
|
||||
onFocus={() => setAutoScroll(true)}
|
||||
onBlur={() => {
|
||||
setAutoScroll(false);
|
||||
setTimeout(() => setPromptHints([]), 100);
|
||||
setTimeout(() => setPromptHints([]), 500);
|
||||
}}
|
||||
autoFocus={!props?.sideBarShowing}
|
||||
/>
|
||||
@ -604,7 +621,9 @@ export function Home() {
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
config.tightBorder ? styles["tight-container"] : styles.container
|
||||
config.tightBorder && !isMobileScreen()
|
||||
? styles["tight-container"]
|
||||
: styles.container
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
import { Avatar, PromptHints } from "./home";
|
||||
|
||||
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
||||
import { getCurrentCommitId } from "../utils";
|
||||
import { getCurrentVersion } from "../utils";
|
||||
import Link from "next/link";
|
||||
import { UPDATE_URL } from "../constant";
|
||||
import { SearchService, usePromptStore } from "../store/prompt";
|
||||
@ -60,7 +60,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
|
||||
const updateStore = useUpdateStore();
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
const currentId = getCurrentCommitId();
|
||||
const currentId = getCurrentVersion();
|
||||
const remoteId = updateStore.remoteId;
|
||||
const hasNewVersion = currentId !== remoteId;
|
||||
|
||||
@ -267,19 +267,17 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
></input>
|
||||
</SettingItem>
|
||||
|
||||
<div className="no-mobile">
|
||||
<SettingItem title={Locale.Settings.TightBorder}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.tightBorder}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) => (config.tightBorder = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</SettingItem>
|
||||
</div>
|
||||
<SettingItem title={Locale.Settings.TightBorder}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.tightBorder}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) => (config.tightBorder = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</SettingItem>
|
||||
</List>
|
||||
<List>
|
||||
<SettingItem
|
||||
@ -375,7 +373,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
type="range"
|
||||
title={config.historyMessageCount.toString()}
|
||||
value={config.historyMessageCount}
|
||||
min="2"
|
||||
min="0"
|
||||
max="25"
|
||||
step="2"
|
||||
onChange={(e) =>
|
||||
|
@ -3,3 +3,4 @@ export const REPO = "ChatGPT-Next-Web";
|
||||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
||||
export const UPDATE_URL = `${REPO_URL}#%E4%BF%9D%E6%8C%81%E6%9B%B4%E6%96%B0-keep-updated`;
|
||||
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||
|
@ -8,11 +8,12 @@ import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
|
||||
let COMMIT_ID: string | undefined;
|
||||
try {
|
||||
COMMIT_ID = process
|
||||
// .execSync("git describe --tags --abbrev=0")
|
||||
.execSync("git rev-parse --short HEAD")
|
||||
.toString()
|
||||
.trim();
|
||||
} catch (e) {
|
||||
console.error("No git or not from git repo.")
|
||||
console.error("No git or not from git repo.");
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
@ -22,13 +23,13 @@ export const metadata = {
|
||||
title: "ChatGPT Next Web",
|
||||
statusBarStyle: "black-translucent",
|
||||
},
|
||||
themeColor: "#fafafa"
|
||||
themeColor: "#fafafa",
|
||||
};
|
||||
|
||||
function Meta() {
|
||||
const metas = {
|
||||
version: COMMIT_ID ?? "unknown",
|
||||
access: (ACCESS_CODES.size > 0 || IS_IN_DOCKER) ? "enabled" : "disabled",
|
||||
access: ACCESS_CODES.size > 0 || IS_IN_DOCKER ? "enabled" : "disabled",
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -57,6 +57,7 @@ const cn = {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
tw: "繁體中文",
|
||||
es: "Español",
|
||||
},
|
||||
},
|
||||
Avatar: "头像",
|
||||
|
@ -54,11 +54,12 @@ const en: LocaleType = {
|
||||
Close: "Close",
|
||||
},
|
||||
Lang: {
|
||||
Name: "语言",
|
||||
Name: "Language",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
tw: "繁體中文",
|
||||
es: "Español",
|
||||
},
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
|
154
app/locales/es.ts
Normal file
154
app/locales/es.ts
Normal file
@ -0,0 +1,154 @@
|
||||
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 EN from "./en";
|
||||
import TW from "./tw";
|
||||
import ES from "./es";
|
||||
|
||||
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];
|
||||
|
||||
const LANG_KEY = "lang";
|
||||
@ -44,6 +45,8 @@ export function getLang(): Lang {
|
||||
return "cn";
|
||||
} else if (lang.includes("tw")) {
|
||||
return "tw";
|
||||
} else if (lang.includes("es")) {
|
||||
return "es";
|
||||
} else {
|
||||
return "en";
|
||||
}
|
||||
@ -54,4 +57,4 @@ export function changeLang(lang: Lang) {
|
||||
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: "關閉",
|
||||
},
|
||||
Lang: {
|
||||
Name: "語言",
|
||||
Name: "Language",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
tw: "繁體中文",
|
||||
es: "Español",
|
||||
},
|
||||
},
|
||||
Avatar: "大頭貼",
|
||||
|
@ -2,6 +2,10 @@ import type { ChatRequest, ChatReponse } from "./api/openai/typing";
|
||||
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
||||
import Locale from "./locales";
|
||||
|
||||
if (!Array.prototype.at) {
|
||||
require('array.prototype.at/auto');
|
||||
}
|
||||
|
||||
const TIME_OUT_MS = 30000;
|
||||
|
||||
const makeRequestParam = (
|
||||
@ -9,7 +13,7 @@ const makeRequestParam = (
|
||||
options?: {
|
||||
filterBot?: boolean;
|
||||
stream?: boolean;
|
||||
},
|
||||
}
|
||||
): ChatRequest => {
|
||||
let sendMessages = messages.map((v) => ({
|
||||
role: v.role,
|
||||
@ -69,10 +73,9 @@ export async function requestChat(messages: Message[]) {
|
||||
}
|
||||
|
||||
export async function requestUsage() {
|
||||
const res = await requestOpenaiClient("dashboard/billing/credit_grants")(
|
||||
null,
|
||||
"GET",
|
||||
);
|
||||
const res = await requestOpenaiClient(
|
||||
"dashboard/billing/credit_grants?_vercel_no_cache=1"
|
||||
)(null, "GET");
|
||||
|
||||
try {
|
||||
const response = (await res.json()) as {
|
||||
@ -94,7 +97,7 @@ export async function requestChatStream(
|
||||
onMessage: (message: string, done: boolean) => void;
|
||||
onError: (error: Error) => void;
|
||||
onController?: (controller: AbortController) => void;
|
||||
},
|
||||
}
|
||||
) {
|
||||
const req = makeRequestParam(messages, {
|
||||
stream: true,
|
||||
@ -189,7 +192,7 @@ export const ControllerPool = {
|
||||
addController(
|
||||
sessionIndex: number,
|
||||
messageIndex: number,
|
||||
controller: AbortController,
|
||||
controller: AbortController
|
||||
) {
|
||||
const key = this.key(sessionIndex, messageIndex);
|
||||
this.controllers[key] = controller;
|
||||
|
@ -11,6 +11,10 @@ import { trimTopic } from "../utils";
|
||||
|
||||
import Locale from "../locales";
|
||||
|
||||
if (!Array.prototype.at) {
|
||||
require('array.prototype.at/auto');
|
||||
}
|
||||
|
||||
export type Message = ChatCompletionResponseMessage & {
|
||||
date: string;
|
||||
streaming?: boolean;
|
||||
@ -89,7 +93,9 @@ export function isValidNumber(x: number, min: number, max: number) {
|
||||
return typeof x === "number" && x <= max && x >= min;
|
||||
}
|
||||
|
||||
export function filterConfig(config: ModelConfig): Partial<ModelConfig> {
|
||||
export function filterConfig(oldConfig: ModelConfig): Partial<ModelConfig> {
|
||||
const config = Object.assign({}, oldConfig);
|
||||
|
||||
const validator: {
|
||||
[k in keyof ModelConfig]: (x: ModelConfig[keyof ModelConfig]) => boolean;
|
||||
} = {
|
||||
@ -103,7 +109,7 @@ export function filterConfig(config: ModelConfig): Partial<ModelConfig> {
|
||||
return isValidNumber(x as number, -2, 2);
|
||||
},
|
||||
temperature(x) {
|
||||
return isValidNumber(x as number, 0, 1);
|
||||
return isValidNumber(x as number, 0, 2);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { FETCH_COMMIT_URL } from "../constant";
|
||||
import { getCurrentCommitId } from "../utils";
|
||||
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
|
||||
import { getCurrentVersion } from "../utils";
|
||||
|
||||
export interface UpdateStore {
|
||||
lastUpdate: number;
|
||||
@ -19,16 +19,17 @@ export const useUpdateStore = create<UpdateStore>()(
|
||||
remoteId: "",
|
||||
|
||||
async getLatestCommitId(force = false) {
|
||||
const overOneHour = Date.now() - get().lastUpdate > 3600 * 1000;
|
||||
const shouldFetch = force || overOneHour;
|
||||
const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
|
||||
const shouldFetch = force || overTenMins;
|
||||
if (!shouldFetch) {
|
||||
return getCurrentCommitId();
|
||||
return getCurrentVersion();
|
||||
}
|
||||
|
||||
try {
|
||||
// const data = await (await fetch(FETCH_TAG_URL)).json();
|
||||
// const remoteId = data[0].name as string;
|
||||
const data = await (await fetch(FETCH_COMMIT_URL)).json();
|
||||
const sha = data[0].sha as string;
|
||||
const remoteId = sha.substring(0, 7);
|
||||
const remoteId = (data[0].sha as string).substring(0, 7);
|
||||
set(() => ({
|
||||
lastUpdate: Date.now(),
|
||||
remoteId,
|
||||
@ -37,13 +38,13 @@ export const useUpdateStore = create<UpdateStore>()(
|
||||
return remoteId;
|
||||
} catch (error) {
|
||||
console.error("[Fetch Upstream Commit Id]", error);
|
||||
return getCurrentCommitId();
|
||||
return getCurrentVersion();
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: UPDATE_KEY,
|
||||
version: 1,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -53,12 +53,13 @@
|
||||
--sidebar-width: 300px;
|
||||
--window-content-width: calc(100% - var(--sidebar-width));
|
||||
--message-max-width: 80%;
|
||||
--full-height: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
:root {
|
||||
--window-width: 100vw;
|
||||
--window-height: 100vh;
|
||||
--window-height: var(--full-height);
|
||||
--sidebar-width: 100vw;
|
||||
--window-content-width: var(--window-width);
|
||||
--message-max-width: 100%;
|
||||
@ -74,20 +75,23 @@
|
||||
@include dark;
|
||||
}
|
||||
}
|
||||
html {
|
||||
height: var(--full-height);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--gray);
|
||||
color: var(--black);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
height: var(--full-height);
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
font-family: "Noto Sans SC", "SF Pro SC", "SF Pro Text", "SF Pro Icons",
|
||||
"PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
||||
"PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
background-color: var(--second);
|
||||
@ -119,6 +123,11 @@ select {
|
||||
cursor: pointer;
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
@ -196,7 +205,7 @@ div.math {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
height: var(--full-height);
|
||||
width: 100vw;
|
||||
background-color: rgba($color: #000000, $alpha: 0.5);
|
||||
display: flex;
|
||||
|
@ -120,33 +120,3 @@
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
.markdown-body pre {
|
||||
filter: invert(1) hue-rotate(90deg) brightness(1.3);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
.markdown-body pre {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
@include light();
|
||||
}
|
||||
|
||||
.light {
|
||||
@include light();
|
||||
}
|
||||
|
||||
.dark {
|
||||
@include dark();
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
@include dark();
|
||||
}
|
||||
}
|
||||
|
16
app/utils.ts
16
app/utils.ts
@ -2,15 +2,7 @@ import { showToast } from "./components/ui-lib";
|
||||
import Locale from "./locales";
|
||||
|
||||
export function trimTopic(topic: string) {
|
||||
const s = topic.split("");
|
||||
let lastChar = s.at(-1); // 获取 s 的最后一个字符
|
||||
let pattern = /[,。!?、,.!?]/; // 定义匹配中文和英文标点符号的正则表达式
|
||||
while (lastChar && pattern.test(lastChar!)) {
|
||||
s.pop();
|
||||
lastChar = s.at(-1);
|
||||
}
|
||||
|
||||
return s.join("");
|
||||
return topic.replace(/[,。!?、,.!?]*$/, "");
|
||||
}
|
||||
|
||||
export function copyToClipboard(text: string) {
|
||||
@ -45,6 +37,10 @@ export function isIOS() {
|
||||
return /iphone|ipad|ipod/.test(userAgent);
|
||||
}
|
||||
|
||||
export function isMobileScreen() {
|
||||
return window.innerWidth <= 600;
|
||||
}
|
||||
|
||||
export function selectOrCopy(el: HTMLElement, content: string) {
|
||||
const currentSelection = window.getSelection();
|
||||
|
||||
@ -72,7 +68,7 @@ export function queryMeta(key: string, defaultValue?: string): string {
|
||||
}
|
||||
|
||||
let currentId: string;
|
||||
export function getCurrentCommitId() {
|
||||
export function getCurrentVersion() {
|
||||
if (currentId) {
|
||||
return currentId;
|
||||
}
|
||||
|
@ -23,9 +23,9 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.5",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"rehype-prism-plus": "^1.5.1",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"sass": "^1.59.2",
|
||||
@ -39,6 +39,7 @@
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-katex": "^3.0.0",
|
||||
"@types/spark-md5": "^3.0.2",
|
||||
"array.prototype.at": "^1.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-next": "13.2.3",
|
||||
|
@ -1,10 +1,14 @@
|
||||
import fetch from "node-fetch";
|
||||
import fs from "fs/promises";
|
||||
|
||||
const CN_URL =
|
||||
const RAW_CN_URL =
|
||||
"https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
|
||||
const EN_URL =
|
||||
const CN_URL =
|
||||
"https://cdn.jsdelivr.net/gh/PlexPt/awesome-chatgpt-prompts-zh@main/prompts-zh.json";
|
||||
const RAW_EN_URL =
|
||||
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
|
||||
const EN_URL =
|
||||
"https://cdn.jsdelivr.net/gh/f/awesome-chatgpt-prompts@main/prompts.csv";
|
||||
const FILE = "./public/prompts.json";
|
||||
|
||||
async function fetchCN() {
|
||||
|
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"
|
||||
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:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2"
|
||||
|
Loading…
Reference in New Issue
Block a user