diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b7e0de26d..a7a29644d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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 - \ No newline at end of file + diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 000000000..1c9dc413c --- /dev/null +++ b/.github/workflows/sync.yml @@ -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 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..bd3337f97 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f97618e70..6f7547b21 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 5952aa33a..d026e61a5 100644 --- a/README.md +++ b/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) diff --git a/app/api/openai/route.ts b/app/api/openai/route.ts index 5bc317e55..5ddb0f4cb 100644 --- a/app/api/openai/route.ts +++ b/app/api/openai/route.ts @@ -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( diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 87231feee..764805d80 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -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; diff --git a/app/components/home.tsx b/app/components/home.tsx index da65b1d63..210e4d740 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -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)} /> ))} @@ -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) => { 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) => { if (shouldSubmit(e)) { onUserSubmit(); e.preventDefault(); @@ -284,9 +290,7 @@ export function Chat(props: { // for auto-scroll const latestMessageRef = useRef(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); + }} > @@ -456,7 +473,7 @@ export function Chat(props: { ); })} -
+
-
@@ -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 (
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 }) { > -
- - - updateConfig( - (config) => (config.tightBorder = e.currentTarget.checked), - ) - } - > - -
+ + + updateConfig( + (config) => (config.tightBorder = e.currentTarget.checked), + ) + } + > + void }) { type="range" title={config.historyMessageCount.toString()} value={config.historyMessageCount} - min="2" + min="0" max="25" step="2" onChange={(e) => diff --git a/app/constant.ts b/app/constant.ts index 818ef1fbe..fff77607e 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -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`; diff --git a/app/layout.tsx b/app/layout.tsx index 3a4140460..32d3576c5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 239da23fa..737ccad45 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -57,6 +57,7 @@ const cn = { cn: "简体中文", en: "English", tw: "繁體中文", + es: "Español", }, }, Avatar: "头像", diff --git a/app/locales/en.ts b/app/locales/en.ts index 296992435..156c03616 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -54,11 +54,12 @@ const en: LocaleType = { Close: "Close", }, Lang: { - Name: "语言", + Name: "Language", Options: { cn: "简体中文", en: "English", tw: "繁體中文", + es: "Español", }, }, Avatar: "Avatar", diff --git a/app/locales/es.ts b/app/locales/es.ts new file mode 100644 index 000000000..3f7ad1bc7 --- /dev/null +++ b/app/locales/es.ts @@ -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; \ No newline at end of file diff --git a/app/locales/index.ts b/app/locales/index.ts index 5e5d38fe9..eb96408c7 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -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()]; \ No newline at end of file diff --git a/app/locales/tw.ts b/app/locales/tw.ts index e63c57a6e..cfba7add5 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -53,11 +53,12 @@ const tw: LocaleType = { Close: "關閉", }, Lang: { - Name: "語言", + Name: "Language", Options: { cn: "简体中文", en: "English", tw: "繁體中文", + es: "Español", }, }, Avatar: "大頭貼", diff --git a/app/requests.ts b/app/requests.ts index d173eb0de..56fd6cb5b 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -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; diff --git a/app/store/app.ts b/app/store/app.ts index 118e9ed6c..cc52a3c79 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -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 { +export function filterConfig(oldConfig: ModelConfig): Partial { + 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 { return isValidNumber(x as number, -2, 2); }, temperature(x) { - return isValidNumber(x as number, 0, 1); + return isValidNumber(x as number, 0, 2); }, }; diff --git a/app/store/update.ts b/app/store/update.ts index 118ea3cea..97fb343c3 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -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()( 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()( return remoteId; } catch (error) { console.error("[Fetch Upstream Commit Id]", error); - return getCurrentCommitId(); + return getCurrentVersion(); } }, }), { name: UPDATE_KEY, version: 1, - } - ) + }, + ), ); diff --git a/app/styles/globals.scss b/app/styles/globals.scss index b4b25b5cd..6637016a1 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -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; diff --git a/app/styles/prism.scss b/app/styles/prism.scss index 0d011ce0e..65ee8b5fc 100644 --- a/app/styles/prism.scss +++ b/app/styles/prism.scss @@ -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(); - } -} diff --git a/app/utils.ts b/app/utils.ts index b32332322..1fb3d3166 100644 --- a/app/utils.ts +++ b/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; } diff --git a/package.json b/package.json index eb17000ed..7c6832eda 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/fetch-prompts.mjs b/scripts/fetch-prompts.mjs index daebfe7a6..3c4801443 100644 --- a/scripts/fetch-prompts.mjs +++ b/scripts/fetch-prompts.mjs @@ -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() { diff --git a/yarn.lock b/yarn.lock index 9f98a244d..246b818b7 100644 --- a/yarn.lock +++ b/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"