diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 6b9565588..7749e6e9e 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -9,7 +9,7 @@ const serverConfig = getServerSideConfig(); const DANGER_CONFIG = { needCode: serverConfig.needCode, hideUserApiKey: serverConfig.hideUserApiKey, - enableGPT4: serverConfig.enableGPT4, + disableGPT4: serverConfig.disableGPT4, hideBalanceQuery: serverConfig.hideBalanceQuery, }; diff --git a/app/api/openai/[...path]/route.ts b/app/api/openai/[...path]/route.ts index 36f92d0ff..9df005a31 100644 --- a/app/api/openai/[...path]/route.ts +++ b/app/api/openai/[...path]/route.ts @@ -1,3 +1,5 @@ +import { type OpenAIListModelResponse } from "@/app/client/platforms/openai"; +import { getServerSideConfig } from "@/app/config/server"; import { OpenaiPath } from "@/app/constant"; import { prettyObject } from "@/app/utils/format"; import { NextRequest, NextResponse } from "next/server"; @@ -6,6 +8,18 @@ import { requestOpenai } from "../../common"; const ALLOWD_PATH = new Set(Object.values(OpenaiPath)); +function getModels(remoteModelRes: OpenAIListModelResponse) { + const config = getServerSideConfig(); + + if (config.disableGPT4) { + remoteModelRes.data = remoteModelRes.data.filter( + (m) => !m.id.startsWith("gpt-4"), + ); + } + + return remoteModelRes; +} + async function handle( req: NextRequest, { params }: { params: { path: string[] } }, @@ -39,7 +53,18 @@ async function handle( } try { - return await requestOpenai(req); + const response = await requestOpenai(req); + + // list models + if (subpath === OpenaiPath.ListModelPath && response.status === 200) { + const resJson = (await response.json()) as OpenAIListModelResponse; + const availableModels = getModels(resJson); + return NextResponse.json(availableModels, { + status: response.status, + }); + } + + return response; } catch (e) { console.error("[OpenAI] ", e); return NextResponse.json(prettyObject(e)); diff --git a/app/client/api.ts b/app/client/api.ts index a8960ff51..08c4bb92a 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -38,9 +38,15 @@ export interface LLMUsage { total: number; } +export interface LLMModel { + name: string; + available: boolean; +} + export abstract class LLMApi { abstract chat(options: ChatOptions): Promise; abstract usage(): Promise; + abstract models(): Promise; } type ProviderName = "openai" | "azure" | "claude" | "palm"; diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index bbd14d613..7e44909e4 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -5,7 +5,7 @@ import { } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; -import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api"; +import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api"; import Locale from "../../locales"; import { EventStreamContentType, @@ -13,6 +13,15 @@ import { } from "@fortaine/fetch-event-source"; import { prettyObject } from "@/app/utils/format"; +export interface OpenAIListModelResponse { + object: string; + data: Array<{ + id: string; + object: string; + root: string; + }>; +} + export class ChatGPTApi implements LLMApi { path(path: string): string { let openaiUrl = useAccessStore.getState().openaiUrl; @@ -22,6 +31,9 @@ export class ChatGPTApi implements LLMApi { if (openaiUrl.endsWith("/")) { openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1); } + if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith("/api/openai")) { + openaiUrl = "https://" + openaiUrl; + } return [openaiUrl, path].join("/"); } @@ -232,5 +244,25 @@ export class ChatGPTApi implements LLMApi { total: total.hard_limit_usd, } as LLMUsage; } + + async models(): Promise { + const res = await fetch(this.path(OpenaiPath.ListModelPath), { + method: "GET", + headers: { + ...getHeaders(), + }, + }); + + const resJson = (await res.json()) as OpenAIListModelResponse; + const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-")); + console.log("[Models]", chatModels); + + return ( + chatModels?.map((m) => ({ + name: m.id, + available: true, + })) || [] + ); + } } export { OpenaiPath }; diff --git a/app/command.ts b/app/command.ts index ba3bb6538..9330d4ff5 100644 --- a/app/command.ts +++ b/app/command.ts @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { useSearchParams } from "react-router-dom"; import Locale from "./locales"; @@ -11,21 +12,22 @@ interface Commands { export function useCommand(commands: Commands = {}) { const [searchParams, setSearchParams] = useSearchParams(); - if (commands === undefined) return; + useEffect(() => { + let shouldUpdate = false; + searchParams.forEach((param, name) => { + const commandName = name as keyof Commands; + if (typeof commands[commandName] === "function") { + commands[commandName]!(param); + searchParams.delete(name); + shouldUpdate = true; + } + }); - let shouldUpdate = false; - searchParams.forEach((param, name) => { - const commandName = name as keyof Commands; - if (typeof commands[commandName] === "function") { - commands[commandName]!(param); - searchParams.delete(name); - shouldUpdate = true; + if (shouldUpdate) { + setSearchParams(searchParams); } - }); - - if (shouldUpdate) { - setSearchParams(searchParams); - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchParams, commands]); } interface ChatCommands { diff --git a/app/components/chat.tsx b/app/components/chat.tsx index a0b0a297a..13105e843 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -42,12 +42,11 @@ import { Theme, useAppConfig, DEFAULT_TOPIC, - ALL_MODELS, + ModelType, } from "../store"; import { copyToClipboard, - downloadAs, selectOrCopy, autoGrowTextArea, useMobileScreen, @@ -62,7 +61,14 @@ import Locale from "../locales"; import { IconButton } from "./button"; import styles from "./chat.module.scss"; -import { ListItem, Modal, showConfirm, showPrompt, showToast } from "./ui-lib"; +import { + ListItem, + Modal, + Selector, + showConfirm, + showPrompt, + showToast, +} from "./ui-lib"; import { useLocation, useNavigate } from "react-router-dom"; import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant"; import { Avatar } from "./emoji"; @@ -173,10 +179,29 @@ function PromptToast(props: { function useSubmitHandler() { const config = useAppConfig(); const submitKey = config.submitKey; + const isComposing = useRef(false); + + useEffect(() => { + const onCompositionStart = () => { + isComposing.current = true; + }; + const onCompositionEnd = () => { + isComposing.current = false; + }; + + window.addEventListener("compositionstart", onCompositionStart); + window.addEventListener("compositionend", onCompositionEnd); + + return () => { + window.removeEventListener("compositionstart", onCompositionStart); + window.removeEventListener("compositionend", onCompositionEnd); + }; + }, []); const shouldSubmit = (e: React.KeyboardEvent) => { if (e.key !== "Enter") return false; - if (e.key === "Enter" && e.nativeEvent.isComposing) return false; + if (e.key === "Enter" && (e.nativeEvent.isComposing || isComposing.current)) + return false; return ( (config.submitKey === SubmitKey.AltEnter && e.altKey) || (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) || @@ -386,16 +411,11 @@ export function ChatActions(props: { // switch model const currentModel = chatStore.currentSession().mask.modelConfig.model; - function nextModel() { - const models = ALL_MODELS.filter((m) => m.available).map((m) => m.name); - const modelIndex = models.indexOf(currentModel); - const nextIndex = (modelIndex + 1) % models.length; - const nextModel = models[nextIndex]; - chatStore.updateCurrentSession((session) => { - session.mask.modelConfig.model = nextModel; - session.mask.syncGlobalConfig = false; - }); - } + const models = useMemo( + () => config.models.filter((m) => m.available).map((m) => m.name), + [config.models], + ); + const [showModelSelector, setShowModelSelector] = useState(false); return (
@@ -467,10 +487,28 @@ export function ChatActions(props: { /> setShowModelSelector(true)} text={currentModel} icon={} /> + + {showModelSelector && ( + ({ + title: m, + value: m, + }))} + onClose={() => setShowModelSelector(false)} + onSelection={(s) => { + if (s.length === 0) return; + chatStore.updateCurrentSession((session) => { + session.mask.modelConfig.model = s[0] as ModelType; + session.mask.syncGlobalConfig = false; + }); + showToast(s[0]); + }} + /> + )}
); } @@ -911,6 +949,7 @@ export function Chat() { const newMessage = await showPrompt( Locale.Chat.Actions.Edit, message.content, + 10, ); chatStore.updateCurrentSession((session) => { const m = session.messages.find( diff --git a/app/components/exporter.module.scss b/app/components/exporter.module.scss index 3fde363f0..c2046ffc0 100644 --- a/app/components/exporter.module.scss +++ b/app/components/exporter.module.scss @@ -186,7 +186,7 @@ box-shadow: var(--card-shadow); border: var(--border-in-light); - * { + *:not(li) { overflow: hidden; } } diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 7765b77aa..f9d86a552 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -1,7 +1,16 @@ +/* eslint-disable @next/next/no-img-element */ import { ChatMessage, useAppConfig, useChatStore } from "../store"; import Locale from "../locales"; import styles from "./exporter.module.scss"; -import { List, ListItem, Modal, Select, showToast } from "./ui-lib"; +import { + List, + ListItem, + Modal, + Select, + showImageModal, + showModal, + showToast, +} from "./ui-lib"; import { IconButton } from "./button"; import { copyToClipboard, downloadAs, useMobileScreen } from "../utils"; @@ -23,6 +32,7 @@ import { DEFAULT_MASK_AVATAR } from "../store/mask"; import { api } from "../client/api"; import { prettyObject } from "../utils/format"; import { EXPORT_MESSAGE_CLASS_NAME } from "../constant"; +import { getClientConfig } from "../config/client"; const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , @@ -369,6 +379,7 @@ export function ImagePreviewer(props: { const previewRef = useRef(null); const copy = () => { + showToast(Locale.Export.Image.Toast); const dom = previewRef.current; if (!dom) return; toBlob(dom).then((blob) => { @@ -393,17 +404,15 @@ export function ImagePreviewer(props: { const isMobile = useMobileScreen(); const download = () => { + showToast(Locale.Export.Image.Toast); const dom = previewRef.current; if (!dom) return; toPng(dom) .then((blob) => { if (!blob) return; - if (isMobile) { - const image = new Image(); - image.src = blob; - const win = window.open(""); - win?.document.write(image.outerHTML); + if (isMobile || getClientConfig()?.isApp) { + showImageModal(blob); } else { const link = document.createElement("a"); link.download = `${props.topic}.png`; diff --git a/app/components/home.tsx b/app/components/home.tsx index b4b190289..96c1b8382 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -27,6 +27,7 @@ import { SideBar } from "./sidebar"; import { useAppConfig } from "../store/config"; import { AuthPage } from "./auth"; import { getClientConfig } from "../config/client"; +import { api } from "../client/api"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -152,8 +153,21 @@ function Screen() { ); } +export function useLoadData() { + const config = useAppConfig(); + + useEffect(() => { + (async () => { + const models = await api.llm.models(); + config.mergeModels(models); + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +} + export function Home() { useSwitchTheme(); + useLoadData(); useEffect(() => { console.log("[Config] got config from build time", getClientConfig()); diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 4db5f573b..3168641c7 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -12,6 +12,7 @@ import mermaid from "mermaid"; import LoadingIcon from "../icons/three-dots.svg"; import React from "react"; import { useDebouncedCallback, useThrottledCallback } from "use-debounce"; +import { showImageModal } from "./ui-lib"; export function Mermaid(props: { code: string }) { const ref = useRef(null); @@ -37,11 +38,13 @@ export function Mermaid(props: { code: string }) { if (!svg) return; const text = new XMLSerializer().serializeToString(svg); const blob = new Blob([text], { type: "image/svg+xml" }); - const url = URL.createObjectURL(blob); - const win = window.open(url); - if (win) { - win.onload = () => URL.revokeObjectURL(url); - } + console.log(blob); + // const url = URL.createObjectURL(blob); + // const win = window.open(url); + // if (win) { + // win.onload = () => URL.revokeObjectURL(url); + // } + showImageModal(URL.createObjectURL(blob)); } if (hasError) { diff --git a/app/components/mask.tsx b/app/components/mask.tsx index ea7cf3a53..be68c00ed 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -30,7 +30,7 @@ import { useNavigate } from "react-router-dom"; import chatStyle from "./chat.module.scss"; import { useEffect, useState } from "react"; -import { downloadAs, readFromFile } from "../utils"; +import { copyToClipboard, downloadAs, readFromFile } from "../utils"; import { Updater } from "../typing"; import { ModelConfigList } from "./model-config"; import { FileName, Path } from "../constant"; @@ -65,6 +65,11 @@ export function MaskConfig(props: { }); }; + const copyMaskLink = () => { + const maskLink = `${location.protocol}//${location.host}/#${Path.NewChat}?mask=${props.mask.id}`; + copyToClipboard(maskLink); + }; + const globalConfig = useAppConfig(); return ( @@ -125,6 +130,20 @@ export function MaskConfig(props: { }} > + + {!props.shouldSyncFromGlobal ? ( + + } + text={Locale.Mask.Config.Share.Action} + onClick={copyMaskLink} + /> + + ) : null} + {props.shouldSyncFromGlobal ? ( void) => void; }) { + const config = useAppConfig(); + return ( <> @@ -22,7 +24,7 @@ export function ModelConfigList(props: { ); }} > - {ALL_MODELS.map((v) => ( + {config.models.map((v) => ( diff --git a/app/components/new-chat.tsx b/app/components/new-chat.tsx index 64c4703a0..710664af5 100644 --- a/app/components/new-chat.tsx +++ b/app/components/new-chat.tsx @@ -15,6 +15,7 @@ import { useAppConfig, useChatStore } from "../store"; import { MaskAvatar } from "./mask"; import { useCommand } from "../command"; import { showConfirm } from "./ui-lib"; +import { BUILTIN_MASK_STORE } from "../masks"; function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) { const xmin = Math.max(aRect.x, bRect.x); @@ -93,14 +94,17 @@ export function NewChat() { const { state } = useLocation(); const startChat = (mask?: Mask) => { - chatStore.newSession(mask); - setTimeout(() => navigate(Path.Chat), 1); + setTimeout(() => { + chatStore.newSession(mask); + navigate(Path.Chat); + }, 10); }; useCommand({ mask: (id) => { try { - const mask = maskStore.get(parseInt(id)); + const intId = parseInt(id); + const mask = maskStore.get(intId) ?? BUILTIN_MASK_STORE.get(intId); startChat(mask ?? undefined); } catch { console.error("[New Chat] failed to create chat from mask id=", id); diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 1ee7316ad..5980a34ef 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -340,6 +340,10 @@ export function Settings() { }; const [loadingUsage, setLoadingUsage] = useState(false); function checkUsage(force = false) { + if (accessStore.hideBalanceQuery) { + return; + } + setLoadingUsage(true); updateStore.updateUsage(force).finally(() => { setLoadingUsage(false); @@ -538,10 +542,12 @@ export function Settings() { } > + + + + + + updateConfig( + (config) => + (config.hideBuiltinMasks = e.currentTarget.checked), + ) + } + > + @@ -577,19 +599,34 @@ export function Settings() { )} {!accessStore.hideUserApiKey ? ( - - { - accessStore.updateToken(e.currentTarget.value); - }} - /> - + <> + + + accessStore.updateOpenAiUrl(e.currentTarget.value) + } + > + + + { + accessStore.updateToken(e.currentTarget.value); + }} + /> + + ) : null} {!accessStore.hideBalanceQuery ? ( @@ -617,22 +654,6 @@ export function Settings() { )} ) : null} - - {!accessStore.hideUserApiKey ? ( - - - accessStore.updateOpenAiUrl(e.currentTarget.value) - } - > - - ) : null} diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index 5a961ef20..67faabbe3 100644 --- a/app/components/ui-lib.module.scss +++ b/app/components/ui-lib.module.scss @@ -62,6 +62,7 @@ box-shadow: var(--card-shadow); margin-bottom: 20px; animation: slide-in ease 0.3s; + background: var(--white); } .list .list-item:last-child { @@ -72,11 +73,26 @@ box-shadow: var(--card-shadow); background-color: var(--white); border-radius: 12px; - width: 60vw; + width: 80vw; + max-width: 900px; + min-width: 300px; animation: slide-in ease 0.3s; --modal-padding: 20px; + &-max { + width: 95vw; + max-width: unset; + height: 95vh; + display: flex; + flex-direction: column; + + .modal-content { + max-height: unset !important; + flex-grow: 1; + } + } + .modal-header { padding: var(--modal-padding); display: flex; @@ -89,11 +105,19 @@ font-size: 16px; } - .modal-close-btn { - cursor: pointer; + .modal-header-actions { + display: flex; - &:hover { - filter: brightness(1.2); + .modal-header-action { + cursor: pointer; + + &:not(:last-child) { + margin-right: 20px; + } + + &:hover { + filter: brightness(1.2); + } } } } @@ -242,9 +266,40 @@ resize: none; outline: none; box-sizing: border-box; - min-height: 30vh; &:focus { border: 1px solid var(--primary); } } + +.selector { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 999; + + &-content { + .list { + overflow: hidden; + + .list-item { + cursor: pointer; + background-color: var(--white); + + &:hover { + filter: brightness(0.95); + } + + &:active { + filter: brightness(0.9); + } + } + } + } +} diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index 44d89c3fc..b96809123 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @next/next/no-img-element */ import styles from "./ui-lib.module.scss"; import LoadingIcon from "../icons/three-dots.svg"; import CloseIcon from "../icons/close.svg"; @@ -6,6 +7,8 @@ import EyeOffIcon from "../icons/eye-off.svg"; import DownIcon from "../icons/down.svg"; import ConfirmIcon from "../icons/confirm.svg"; import CancelIcon from "../icons/cancel.svg"; +import MaxIcon from "../icons/max.svg"; +import MinIcon from "../icons/min.svg"; import Locale from "../locales"; @@ -44,9 +47,13 @@ export function ListItem(props: { children?: JSX.Element | JSX.Element[]; icon?: JSX.Element; className?: string; + onClick?: () => void; }) { return ( -
+
{props.icon &&
{props.icon}
}
@@ -93,6 +100,7 @@ interface ModalProps { title: string; children?: any; actions?: JSX.Element[]; + defaultMax?: boolean; onClose?: () => void; } export function Modal(props: ModalProps) { @@ -111,13 +119,30 @@ export function Modal(props: ModalProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const [isMax, setMax] = useState(!!props.defaultMax); + return ( -
+
{props.title}
-
- +
+
setMax(!isMax)} + > + {isMax ? : } +
+
+ +
@@ -321,6 +346,7 @@ export function showConfirm(content: any) { function PromptInput(props: { value: string; onChange: (value: string) => void; + rows?: number; }) { const [input, setInput] = useState(props.value); const onInput = (value: string) => { @@ -334,11 +360,12 @@ function PromptInput(props: { autoFocus value={input} onInput={(e) => onInput(e.currentTarget.value)} + rows={props.rows ?? 3} > ); } -export function showPrompt(content: any, value = "") { +export function showPrompt(content: any, value = "", rows = 3) { const div = document.createElement("div"); div.className = "modal-mask"; document.body.appendChild(div); @@ -386,8 +413,60 @@ export function showPrompt(content: any, value = "") { (userInput = val)} value={value} + rows={rows} > , ); }); } + +export function showImageModal(img: string) { + showModal({ + title: Locale.Export.Image.Modal, + children: ( +
+ preview +
+ ), + }); +} + +export function Selector(props: { + items: Array<{ + title: string; + subTitle?: string; + value: T; + }>; + onSelection?: (selection: T[]) => void; + onClose?: () => void; + multiple?: boolean; +}) { + return ( +
props.onClose?.()}> +
+ + {props.items.map((item, i) => { + return ( + { + props.onSelection?.([item.value]); + props.onClose?.(); + }} + > + ); + })} + +
+
+ ); +} diff --git a/app/config/server.ts b/app/config/server.ts index 5479995e9..6eab9ebec 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -46,7 +46,7 @@ export const getServerSideConfig = () => { proxyUrl: process.env.PROXY_URL, isVercel: !!process.env.VERCEL, hideUserApiKey: !!process.env.HIDE_USER_API_KEY, - enableGPT4: !process.env.DISABLE_GPT4, + disableGPT4: !!process.env.DISABLE_GPT4, hideBalanceQuery: !!process.env.HIDE_BALANCE_QUERY, }; }; diff --git a/app/constant.ts b/app/constant.ts index b01fd788d..df2bc52a5 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -53,6 +53,7 @@ export const OpenaiPath = { ChatPath: "v1/chat/completions", UsagePath: "dashboard/billing/usage", SubsPath: "dashboard/billing/subscription", + ListModelPath: "v1/models", }; export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang @@ -61,3 +62,70 @@ You are ChatGPT, a large language model trained by OpenAI. Knowledge cutoff: 2021-09 Current model: {{model}} Current time: {{time}}`; + +export const DEFAULT_MODELS = [ + { + name: "gpt-4", + available: true, + }, + { + name: "gpt-4-0314", + available: true, + }, + { + name: "gpt-4-0613", + available: true, + }, + { + name: "gpt-4-32k", + available: true, + }, + { + name: "gpt-4-32k-0314", + available: true, + }, + { + name: "gpt-4-32k-0613", + available: true, + }, + { + name: "gpt-3.5-turbo", + available: true, + }, + { + name: "gpt-3.5-turbo-0301", + available: true, + }, + { + name: "gpt-3.5-turbo-0613", + available: true, + }, + { + name: "gpt-3.5-turbo-16k", + available: true, + }, + { + name: "gpt-3.5-turbo-16k-0613", + available: true, + }, + { + name: "qwen-v1", // 通义千问 + available: false, + }, + { + name: "ernie", // 文心一言 + available: false, + }, + { + name: "spark", // 讯飞星火 + available: false, + }, + { + name: "llama", // llama + available: false, + }, + { + name: "chatglm", // chatglm-6b + available: false, + }, +] as const; diff --git a/app/locales/ar.ts b/app/locales/ar.ts index 6ece142b4..b3b5c0216 100644 --- a/app/locales/ar.ts +++ b/app/locales/ar.ts @@ -130,8 +130,10 @@ const ar: PartialLocaleType = { SubTitle: "معاينة Markdown في فقاعة", }, Mask: { - Title: "شاشة تظهر الأقنعة", - SubTitle: "عرض شاشة تظهر الأقنعة قبل بدء الدردشة الجديدة", + Splash: { + Title: "شاشة تظهر الأقنعة", + SubTitle: "عرض شاشة تظهر الأقنعة قبل بدء الدردشة الجديدة", + }, }, Prompt: { Disable: { diff --git a/app/locales/bn.ts b/app/locales/bn.ts new file mode 100644 index 000000000..065f4276a --- /dev/null +++ b/app/locales/bn.ts @@ -0,0 +1,338 @@ +import { SubmitKey } from "../store/config"; +import { PartialLocaleType } from "./index"; + +const bn: PartialLocaleType = { + WIP: "শীঘ্রই আসছে...", + Error: { + Unauthorized: + "অননুমোদিত অ্যাক্সেস, অনুগ্রহ করে [অথোরাইজশন](/#/auth) পৃষ্ঠায় অ্যাক্সেস কোড ইনপুট করুন।", + }, + Auth: { + Title: "একটি অ্যাক্সেস কোড প্রয়োজন", + Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন", + Input: "অ্যাক্সেস কোড", + Confirm: "নিশ্চিত করুন", + Later: "পরে", + }, + ChatItem: { + ChatItemCount: (count: number) => `${count} টি বার্তা`, + }, + Chat: { + SubTitle: (count: number) => `${count} টি বার্তা`, + Actions: { + ChatList: "চ্যাট তালিকায় যান", + CompressedHistory: "সংক্ষিপ্ত ইতিহাস মেমোরি প্রম্পট", + Export: "সমস্ত বার্তা মার্কডাউন হিসাবে রপ্তানি করুন", + Copy: "কপি", + Stop: "বন্ধ করুন", + Retry: "পুনরায় চেষ্টা করুন", + Pin: "পিন করুন", + PinToastContent: "পিন করা হয়েছে ২টি বার্তা প্রম্পটে", + PinToastAction: "দেখুন", + Delete: "মুছে ফেলুন", + Edit: "সম্পাদন করুন", + }, + Commands: { + new: "নতুন চ্যাট শুরু করুন", + newm: "মাস্ক সহ নতুন চ্যাট শুরু করুন", + next: "পরবর্তী চ্যাট", + prev: "পূর্ববর্তী চ্যাট", + clear: "সংশ্লিষ্টতাবদ্ধকরণ পরিষ্কার করুন", + del: "চ্যাট মুছুন", + }, + InputActions: { + Stop: "বন্ধ করুন", + ToBottom: "সর্বশেষতম দিকে", + Theme: { + auto: "অটো", + light: "হালকা থিম", + dark: "ডার্ক থিম", + }, + Prompt: "প্রম্পটগুলিতে", + Masks: "মাস্কগুলি", + Clear: "সংশ্লিষ্টতাবদ্ধকরণ পরিষ্কার করুন", + Settings: "সেটিংস", + }, + Rename: "চ্যাট পুনঃনামকরণ করুন", + Typing: "টাইপিং...", + Input: (submitKey: string) => { + var inputHints = `${submitKey} to send`; + if (submitKey === String(SubmitKey.Enter)) { + inputHints += ", Shift + Enter to wrap"; + } + return inputHints + ", / to search prompts, : to use commands"; + }, + Send: "প্রেরণ করুন", + Config: { + Reset: "ডিফল্টে রিসেট করুন", + SaveAs: "মাস্ক হিসাবে সংরক্ষণ করুন", + }, + }, + Export: { + Title: "বার্তা রপ্তানিকরণ", + Copy: "সমস্তটি কপি করুন", + Download: "ডাউনলোড করুন", + MessageFromYou: "আপনার বার্তা", + MessageFromChatGPT: "চ্যাটজিপিটির বার্তা", + Share: "শেয়ার করুন শেয়ারজিপিটি তে", + Format: { + Title: "রপ্তানি ফরম্যাট", + SubTitle: "মার্কডাউন বা পিএনজি চিত্র", + }, + IncludeContext: { + Title: "মাস্ক অন্তর্ভুক্ত করুন", + SubTitle: "মাস্কগুলি সংরক্ষণ করবেন না কি", + }, + Steps: { + Select: "নির্বাচন করুন", + Preview: "প্রিভিউ করুন", + }, + }, + Select: { + Search: "অনুসন্ধান করুন", + All: "সমস্তটি নির্বাচন করুন", + Latest: "সর্বশেষতমটি নির্বাচন করুন", + Clear: "পরিষ্কার করুন", + }, + Memory: { + Title: "মেমোরি প্রম্পট", + EmptyContent: "এখনও কিছুই নেই।", + Send: "মেমোরি প্রেরণ করুন", + Copy: "মেমোরি কপি করুন", + Reset: "পুনরায় নিশ্চিত করুন", + ResetConfirm: + "রিসেট করলে বর্তমান চ্যাট ইতিহাস এবং ঐতিহাসিক মেমোরি মুছে যাবে। পুনরায় নির্দিষ্ট করতে চান তা নিশ্চিত করতে চান?", + }, + Home: { + NewChat: "নতুন চ্যাট", + DeleteChat: "নির্বাচিত সংলাপটি মুছতে নিশ্চিত করুন?", + DeleteToast: "চ্যাটটি মুছেছেন", + Revert: "পুনরায়", + }, + Settings: { + Title: "সেটিংস", + SubTitle: "সমস্ত সেটিংস", + Danger: { + Reset: { + Title: "সমস্ত সেটিংস পুনঃনির্দেশ দিন", + SubTitle: "সকল সেটিংস ডিফল্টে পুনঃনির্দেশ দিতে", + Action: "পুনঃনির্দেশ দিন", + Confirm: "সমস্ত সেটিংস ডিফল্টে পুনঃনির্দেশ করতে নিশ্চিত করতে?", + }, + Clear: { + Title: "সমস্ত তথ্য মুছুন", + SubTitle: "সমস্ত বার্তা এবং সেটিংস মুছুন", + Action: "মুছুন", + Confirm: "সমস্ত বার্তা এবং সেটিংস মুছে ফেলতে নিশ্চিত করতে?", + }, + }, + Lang: { + Name: "বাংলা", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` + All: "সমস্ত ভাষা", + }, + Avatar: "অবতার", + FontSize: { + Title: "ফন্ট সাইজ", + SubTitle: "চ্যাট সামগ্রীর ফন্ট সাইজ সংশোধন করুন", + }, + + InputTemplate: { + Title: "ইনপুট টেমপ্লেট", + SubTitle: "নতুনতম বার্তা এই টেমপ্লেটে পূরণ হবে", + }, + + Update: { + Version: (x: string) => `Version: ${x}`, + IsLatest: "Latest version", + CheckUpdate: "Check Update", + IsChecking: "Checking update...", + FoundUpdate: (x: string) => `Found new version: ${x}`, + GoToUpdate: "Update", + }, + SendKey: "প্রেরণ চাবি", + Theme: "থিম", + TightBorder: "সঙ্গতি সীমা", + SendPreviewBubble: { + Title: "প্রিভিউ বুলবুল প্রেরণ করুন", + SubTitle: "বুলবুলে মার্কডাউন প্রিভিউ করুন", + }, + Mask: { + Splash: { + Title: "মাস্ক স্প্ল্যাশ স্ক্রিন", + SubTitle: + "নতুন চ্যাট শুরু করার আগে মাস্ক স্প্ল্যাশ স্ক্রিন প্রদর্শন করুন", + }, + Builtin: { + Title: "মূলত মাস্ক গোপন করুন", + SubTitle: "মাস্ক তালিকা থেকে মূলত মাস্কগুলি লুকান", + }, + }, + Prompt: { + Disable: { + Title: "অটো-সম্পূর্ণতা নিষ্ক্রিয় করুন", + SubTitle: "অটো-সম্পূর্ণতা চালু করতে / ইনপুট করুন", + }, + List: "প্রম্পট তালিকা", + ListCount: (builtin: number, custom: number) => + `${builtin} built-in, ${custom} user-defined`, + Edit: "সম্পাদন করুন", + Modal: { + Title: "প্রম্পট তালিকা", + Add: "একটি যোগ করুন", + Search: "সন্ধান প্রম্পট", + }, + EditModal: { + Title: "সম্পাদন করুন প্রম্পট", + }, + }, + HistoryCount: { + Title: "সংযুক্ত বার্তা সংখ্যা", + SubTitle: "প্রতি অনুরোধে প্রেরণ করা গেলে প্রেরণ করা হবে", + }, + CompressThreshold: { + Title: "ইতিহাস সঙ্কুচিত করার সীমা", + SubTitle: + "নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে", + }, + Token: { + Title: "অ্যাপি কী", + SubTitle: "অ্যাক্সেস কোড সীমা উপেক্ষা করতে আপনার কীটি ব্যবহার করুন", + Placeholder: "OpenAI API কী", + }, + Usage: { + Title: "একাউন্ট ব্যালেন্স", + SubTitle(used: any, total: any) { + return `এই মাসে ব্যবহৃত $${used}, সাবস্ক্রিপশন $${total}`; + }, + IsChecking: "চেক করা হচ্ছে...", + Check: "চেক", + NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন", + }, + AccessCode: { + Title: "অ্যাক্সেস কোড", + SubTitle: "অ্যাক্সেস নিয়ন্ত্রণ সক্রিয়", + Placeholder: "অ্যাক্সেস কোড প্রয়োজন", + }, + Endpoint: { + Title: "ইনটারপয়েন্ট", + SubTitle: "কাস্টম এন্ডপয়েন্টটি হতে হবে http(s):// দিয়ে শুরু হতে হবে", + }, + Model: "মডেল", + Temperature: { + Title: "তাপমাত্রা", + SubTitle: "আরতি মান বেশি করলে বেশি এলোমেলো আউটপুট হবে", + }, + TopP: { + Title: "শীর্ষ পি", + SubTitle: "তাপমাত্রা সঙ্গে এই মান পরিবর্তন করবেন না", + }, + MaxTokens: { + Title: "সর্বাধিক টোকেন", + SubTitle: "ইনপুট টোকেন এবং উৎপাদিত টোকেনের সর্বাধিক দৈর্ঘ্য", + }, + PresencePenalty: { + Title: "উপস্থিতির জরিমানা", + SubTitle: "আরতি মান বেশি করলে নতুন বিষয়গুলি সম্ভাব্যতা বাড়াতে পারে", + }, + FrequencyPenalty: { + Title: "ফ্রিকুয়েন্সি জরিমানা", + SubTitle: + "আরতি মান বাড়ালে একই লাইন পুনরায় ব্যাবহার করার সম্ভাবনা হ্রাস পায়", + }, + }, + Store: { + DefaultTopic: "নতুন সংলাপ", + BotHello: "হ্যালো! আজকে আপনাকে কিভাবে সাহায্য করতে পারি?", + Error: "কিছু নিয়ে ভুল হয়েছে, পরে আবার চেষ্টা করুন।", + Prompt: { + History: (content: string) => + "এটি চ্যাট ইতিহাসের সংক্ষিপ্ত সংকলনের মতো: " + content, + Topic: + "আমাদের সংলাপটির চার থেকে পাঁচ শব্দের একটি শিরোনাম তৈরি করুন যা আমাদের আলাপের সংক্ষিপ্তসার হিসাবে যোগ হবে না, যেমন অভিবৃত্তি, বিন্যাস, উদ্ধৃতি, পূর্বচালক চিহ্ন, পূর্বরোবক্তির যেকোনো চিহ্ন বা অতিরিক্ত পাঠ। মেয়াদশেষ উদ্ধৃতি চেষ্টা করুন।", + Summarize: + "২০০ শব্দের লম্বা হয়ে মুহূর্তে আলোচনা সংক্ষেপের রপ্তানি করুন, যেটি ভবিষ্যতের প্রম্পট হিসাবে ব্যবহার করবেন।", + }, + }, + Copy: { + Success: "ক্লিপবোর্ডে কপি করা হয়েছে", + Failed: "কপি ব্যর্থ, অনুমতি প্রদান করার জন্য অনুমতি প্রদান করুন", + }, + Context: { + Toast: (x: any) => `With ${x} contextual prompts`, + Edit: "বর্তমান চ্যাট সেটিংস", + Add: "একটি প্রম্পট যোগ করুন", + Clear: "সঙ্গতি পরিস্কার করুন", + Revert: "পূর্ববর্তী অবস্থানে ফিরে যান", + }, + Plugin: { + Name: "প্লাগইন", + }, + Mask: { + Name: "মাস্ক", + Page: { + Title: "প্রম্পট টেমপ্লেট", + SubTitle: (count: number) => `${count} টি প্রম্পট টেমপ্লেট`, + Search: "টেমপ্লেট অনুসন্ধান করুন", + Create: "তৈরি করুন", + }, + Item: { + Info: (count: number) => `${count} প্রম্পট`, + Chat: "চ্যাট", + View: "দেখুন", + Edit: "সম্পাদন করুন", + Delete: "মুছে ফেলুন", + DeleteConfirm: "মুছে ফেলতে নিশ্চিত করুন?", + }, + EditModal: { + Title: (readonly: boolean) => + `প্রম্পট টেমপ্লেট সম্পাদন করুন ${readonly ? "(readonly)" : ""}`, + Download: "ডাউনলোড করুন", + Clone: "ক্লোন করুন", + }, + Config: { + Avatar: "বট অবতার", + Name: "বটের নাম", + Sync: { + Title: "গ্লোবাল কনফিগ ব্যবহার করুন", + SubTitle: "এই চ্যাটে গ্লোবাল কনফিগ ব্যবহার করুন", + Confirm: + "গ্লোবাল কনফিগ দ্বারা কাস্টম কনফিগ ওভাররাইড করতে নিশ্চিত করতে?", + }, + HideContext: { + Title: "সংশ্লিষ্টতা প্রম্পটগুলি লুকান", + SubTitle: "চ্যাটে সংশ্লিষ্টতা প্রম্পটগুলি দেখাবেন না", + }, + Share: { + Title: "এই মাস্কটি শেয়ার করুন", + SubTitle: "এই মাস্কের একটি লিঙ্ক তৈরি করুন", + Action: "লিঙ্ক কপি করুন", + }, + }, + }, + NewChat: { + Return: "ফিরে যান", + Skip: "শুরু করুন", + Title: "মাস্ক নির্বাচন করুন", + SubTitle: "মাস্কের পিছনে আত্মার সঙ্গে চ্যাট করুন", + More: "আরো খুঁজুন", + NotShow: "এখনও দেখাবেন না", + ConfirmNoShow: + "নিষ্ক্রিয় করতে নিশ্চিত করুন? পরে আপনি এটি সেটিংসে সক্ষম করতে পারবেন।", + }, + + UI: { + Confirm: "নিশ্চিত করুন", + Cancel: "বাতিল করুন", + Close: "বন্ধ করুন", + Create: "তৈরি করুন", + Edit: "সম্পাদন করুন", + }, + Exporter: { + Model: "মডেল", + Messages: "বার্তা", + Topic: "টপিক", + Time: "সময়", + }, +}; + +export default bn; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index b8fa82163..07e87cbe6 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -86,6 +86,10 @@ const cn = { Select: "选取", Preview: "预览", }, + Image: { + Toast: "正在生成截图", + Modal: "长按或右键保存图片", + }, }, Select: { Search: "搜索消息", @@ -156,8 +160,14 @@ const cn = { SubTitle: "在预览气泡中预览 Markdown 内容", }, Mask: { - Title: "面具启动页", - SubTitle: "新建聊天时,展示面具启动页", + Splash: { + Title: "面具启动页", + SubTitle: "新建聊天时,展示面具启动页", + }, + Builtin: { + Title: "隐藏内置面具", + SubTitle: "在所有面具列表中隐藏内置面具", + }, }, Prompt: { Disable: { @@ -291,6 +301,11 @@ const cn = { Title: "隐藏预设对话", SubTitle: "隐藏后预设对话不会出现在聊天界面", }, + Share: { + Title: "分享此面具", + SubTitle: "生成此面具的直达链接", + Action: "复制链接", + }, }, }, NewChat: { diff --git a/app/locales/cs.ts b/app/locales/cs.ts index 9f9afab05..348e16afc 100644 --- a/app/locales/cs.ts +++ b/app/locales/cs.ts @@ -87,8 +87,10 @@ const cs: PartialLocaleType = { SubTitle: "Zobrazit v náhledu bubliny", }, Mask: { - Title: "Úvodní obrazovka Masek", - SubTitle: "Před zahájením nového chatu zobrazte úvodní obrazovku Masek", + Splash: { + Title: "Úvodní obrazovka Masek", + SubTitle: "Před zahájením nového chatu zobrazte úvodní obrazovku Masek", + }, }, Prompt: { Disable: { diff --git a/app/locales/de.ts b/app/locales/de.ts index e049adb3b..7e6f88af9 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -87,8 +87,10 @@ const de: PartialLocaleType = { SubTitle: "Preview markdown in bubble", }, Mask: { - Title: "Mask Splash Screen", - SubTitle: "Show a mask splash screen before starting new chat", + Splash: { + Title: "Mask Splash Screen", + SubTitle: "Show a mask splash screen before starting new chat", + }, }, Prompt: { Disable: { diff --git a/app/locales/en.ts b/app/locales/en.ts index bf2557a6c..9373e2b14 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -1,6 +1,7 @@ import { SubmitKey } from "../store/config"; import { LocaleType } from "./index"; +// if you are adding a new translation, please use PartialLocaleType instead of LocaleType const en: LocaleType = { WIP: "Coming Soon...", Error: { @@ -87,6 +88,10 @@ const en: LocaleType = { Select: "Select", Preview: "Preview", }, + Image: { + Toast: "Capturing Image...", + Modal: "Long press or right click to save image", + }, }, Select: { Search: "Search", @@ -157,8 +162,14 @@ const en: LocaleType = { SubTitle: "Preview markdown in bubble", }, Mask: { - Title: "Mask Splash Screen", - SubTitle: "Show a mask splash screen before starting new chat", + Splash: { + Title: "Mask Splash Screen", + SubTitle: "Show a mask splash screen before starting new chat", + }, + Builtin: { + Title: "Hide Builtin Masks", + SubTitle: "Hide builtin masks in mask list", + }, }, Prompt: { Disable: { @@ -295,6 +306,11 @@ const en: LocaleType = { Title: "Hide Context Prompts", SubTitle: "Do not show in-context prompts in chat", }, + Share: { + Title: "Share This Mask", + SubTitle: "Generate a link to this mask", + Action: "Copy Link", + }, }, }, NewChat: { diff --git a/app/locales/es.ts b/app/locales/es.ts index e7f8cca4c..0971f05c5 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -87,8 +87,10 @@ const es: PartialLocaleType = { SubTitle: "Preview markdown in bubble", }, Mask: { - Title: "Mask Splash Screen", - SubTitle: "Show a mask splash screen before starting new chat", + Splash: { + Title: "Mask Splash Screen", + SubTitle: "Show a mask splash screen before starting new chat", + }, }, Prompt: { Disable: { diff --git a/app/locales/fr.ts b/app/locales/fr.ts index b6b8c0321..72be76e1c 100644 --- a/app/locales/fr.ts +++ b/app/locales/fr.ts @@ -88,9 +88,11 @@ const fr: PartialLocaleType = { SubTitle: "Aperçu du Markdown dans une bulle", }, Mask: { - Title: "Écran de masque", - SubTitle: - "Afficher un écran de masque avant de démarrer une nouvelle discussion", + Splash: { + Title: "Écran de masque", + SubTitle: + "Afficher un écran de masque avant de démarrer une nouvelle discussion", + }, }, Prompt: { Disable: { diff --git a/app/locales/index.ts b/app/locales/index.ts index abdb4eaab..7ece45838 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -13,6 +13,7 @@ import no from "./no"; import cs from "./cs"; import ko from "./ko"; import ar from "./ar"; +import bn from "./bn"; import { merge } from "../utils/merge"; import type { LocaleType } from "./cn"; @@ -34,6 +35,7 @@ const ALL_LANGS = { cs, no, ar, + bn, }; export type Lang = keyof typeof ALL_LANGS; @@ -56,6 +58,7 @@ export const ALL_LANG_OPTIONS: Record = { cs: "Čeština", no: "Nynorsk", ar: "العربية", + bn: "বাংলা", }; const LANG_KEY = "lang"; diff --git a/app/locales/it.ts b/app/locales/it.ts index 8962968a5..acd3a7e93 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -87,8 +87,10 @@ const it: PartialLocaleType = { SubTitle: "Preview markdown in bubble", }, Mask: { - Title: "Mask Splash Screen", - SubTitle: "Show a mask splash screen before starting new chat", + Splash: { + Title: "Mask Splash Screen", + SubTitle: "Show a mask splash screen before starting new chat", + }, }, Prompt: { Disable: { diff --git a/app/locales/jp.ts b/app/locales/jp.ts index d2feca4d8..090a428fa 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -104,8 +104,10 @@ const jp: PartialLocaleType = { SubTitle: "プレビューバブルでマークダウンコンテンツをプレビュー", }, Mask: { - Title: "キャラクターページ", - SubTitle: "新規チャット作成時にキャラクターページを表示する", + Splash: { + Title: "キャラクターページ", + SubTitle: "新規チャット作成時にキャラクターページを表示する", + }, }, Prompt: { Disable: { diff --git a/app/locales/ko.ts b/app/locales/ko.ts index a3a5f73dc..6f5ec7a9a 100644 --- a/app/locales/ko.ts +++ b/app/locales/ko.ts @@ -87,8 +87,10 @@ const ko: PartialLocaleType = { SubTitle: "버블에서 마크다운 미리 보기", }, Mask: { - Title: "마스크 시작 화면", - SubTitle: "새로운 채팅 시작 전에 마스크 시작 화면 표시", + Splash: { + Title: "마스크 시작 화면", + SubTitle: "새로운 채팅 시작 전에 마스크 시작 화면 표시", + }, }, Prompt: { Disable: { diff --git a/app/locales/ru.ts b/app/locales/ru.ts index 9121e2782..06c945859 100644 --- a/app/locales/ru.ts +++ b/app/locales/ru.ts @@ -87,8 +87,10 @@ const ru: PartialLocaleType = { SubTitle: "Предварительный просмотр markdown в пузыре", }, Mask: { - Title: "Экран заставки маски", - SubTitle: "Показывать экран заставки маски перед началом нового чата", + Splash: { + Title: "Экран заставки маски", + SubTitle: "Показывать экран заставки маски перед началом нового чата", + }, }, Prompt: { Disable: { diff --git a/app/locales/tr.ts b/app/locales/tr.ts index e199f115f..2383a5494 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -87,8 +87,10 @@ const tr: PartialLocaleType = { SubTitle: "Preview markdown in bubble", }, Mask: { - Title: "Mask Splash Screen", - SubTitle: "Show a mask splash screen before starting new chat", + Splash: { + Title: "Mask Splash Screen", + SubTitle: "Show a mask splash screen before starting new chat", + }, }, Prompt: { Disable: { diff --git a/app/locales/tw.ts b/app/locales/tw.ts index cb92a81d8..1afb0eb71 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -85,8 +85,10 @@ const tw: PartialLocaleType = { SubTitle: "在预览气泡中预览 Markdown 内容", }, Mask: { - Title: "面具启动页", - SubTitle: "新建聊天时,展示面具启动页", + Splash: { + Title: "面具启动页", + SubTitle: "新建聊天时,展示面具启动页", + }, }, Prompt: { Disable: { diff --git a/app/locales/vi.ts b/app/locales/vi.ts index cc0178b1f..428f93857 100644 --- a/app/locales/vi.ts +++ b/app/locales/vi.ts @@ -87,8 +87,10 @@ const vi: PartialLocaleType = { SubTitle: "Xem trước nội dung markdown bằng bong bóng", }, Mask: { - Title: "Mask Splash Screen", - SubTitle: "Chớp màn hình khi bắt đầu cuộc trò chuyện mới", + Splash: { + Title: "Mask Splash Screen", + SubTitle: "Chớp màn hình khi bắt đầu cuộc trò chuyện mới", + }, }, Prompt: { Disable: { diff --git a/app/store/access.ts b/app/store/access.ts index e9d09bb84..d28064147 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -3,7 +3,6 @@ import { persist } from "zustand/middleware"; import { DEFAULT_API_HOST, StoreKey } from "../constant"; import { getHeaders } from "../client/api"; import { BOT_HELLO } from "./chat"; -import { ALL_MODELS } from "./config"; import { getClientConfig } from "../config/client"; export interface AccessControlStore { @@ -76,14 +75,6 @@ export const useAccessStore = create()( console.log("[Config] got config from server", res); set(() => ({ ...res })); - if (!res.enableGPT4) { - ALL_MODELS.forEach((model) => { - if (model.name.startsWith("gpt-4")) { - (model as any).available = false; - } - }); - } - if ((res as any).botHello) { BOT_HELLO.content = (res as any).botHello; } diff --git a/app/store/chat.ts b/app/store/chat.ts index 4c466a295..222b29c94 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -420,7 +420,7 @@ export const useChatStore = create()( modelConfig.sendMemory && session.memoryPrompt && session.memoryPrompt.length > 0 && - session.lastSummarizeIndex <= clearContextIndex; + session.lastSummarizeIndex > clearContextIndex; const longTermMemoryPrompts = shouldSendLongTermMemory ? [get().getMemoryPrompt()] : []; diff --git a/app/store/config.ts b/app/store/config.ts index dbc5eaebd..f07191401 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -1,7 +1,10 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { LLMModel } from "../client/api"; import { getClientConfig } from "../config/client"; -import { DEFAULT_INPUT_TEMPLATE, StoreKey } from "../constant"; +import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant"; + +export type ModelType = (typeof DEFAULT_MODELS)[number]["name"]; export enum SubmitKey { Enter = "Enter", @@ -29,6 +32,9 @@ export const DEFAULT_CONFIG = { disablePromptHint: false, dontShowMaskSplashScreen: false, // dont show splash screen when create chat + hideBuiltinMasks: false, // dont add builtin masks + + models: DEFAULT_MODELS as any as LLMModel[], modelConfig: { model: "gpt-4" as ModelType, @@ -49,81 +55,11 @@ export type ChatConfig = typeof DEFAULT_CONFIG; export type ChatConfigStore = ChatConfig & { reset: () => void; update: (updater: (config: ChatConfig) => void) => void; + mergeModels: (newModels: LLMModel[]) => void; }; export type ModelConfig = ChatConfig["modelConfig"]; -const ENABLE_GPT4 = true; - -export const ALL_MODELS = [ - { - name: "gpt-4", - available: ENABLE_GPT4, - }, - { - name: "gpt-4-0314", - available: ENABLE_GPT4, - }, - { - name: "gpt-4-0613", - available: ENABLE_GPT4, - }, - { - name: "gpt-4-32k", - available: ENABLE_GPT4, - }, - { - name: "gpt-4-32k-0314", - available: ENABLE_GPT4, - }, - { - name: "gpt-4-32k-0613", - available: ENABLE_GPT4, - }, - { - name: "gpt-3.5-turbo", - available: true, - }, - { - name: "gpt-3.5-turbo-0301", - available: true, - }, - { - name: "gpt-3.5-turbo-0613", - available: true, - }, - { - name: "gpt-3.5-turbo-16k", - available: true, - }, - { - name: "gpt-3.5-turbo-16k-0613", - available: true, - }, - { - name: "qwen-v1", // 通义千问 - available: false, - }, - { - name: "ernie", // 文心一言 - available: false, - }, - { - name: "spark", // 讯飞星火 - available: false, - }, - { - name: "llama", // llama - available: false, - }, - { - name: "chatglm", // chatglm-6b - available: false, - }, -] as const; - -export type ModelType = (typeof ALL_MODELS)[number]["name"]; - export function limitNumber( x: number, min: number, @@ -138,7 +74,8 @@ export function limitNumber( } export function limitModel(name: string) { - return ALL_MODELS.some((m) => m.name === name && m.available) + const allModels = useAppConfig.getState().models; + return allModels.some((m) => m.name === name && m.available) ? name : "gpt-3.5-turbo"; } @@ -178,12 +115,31 @@ export const useAppConfig = create()( updater(config); set(() => config); }, + + mergeModels(newModels) { + const oldModels = get().models; + const modelMap: Record = {}; + + for (const model of oldModels) { + model.available = false; + modelMap[model.name] = model; + } + + for (const model of newModels) { + model.available = true; + modelMap[model.name] = model; + } + + set(() => ({ + models: Object.values(modelMap), + })); + }, }), { name: StoreKey.Config, - version: 3.3, + version: 3.4, migrate(persistedState, version) { - if (version === 3.3) return persistedState as any; + if (version === 3.4) return persistedState as any; const state = persistedState as ChatConfig; state.modelConfig.sendMemory = true; @@ -193,6 +149,7 @@ export const useAppConfig = create()( state.modelConfig.top_p = 1; state.modelConfig.template = DEFAULT_INPUT_TEMPLATE; state.dontShowMaskSplashScreen = false; + state.hideBuiltinMasks = false; return state; }, diff --git a/app/store/mask.ts b/app/store/mask.ts index 6d6377c37..d55400522 100644 --- a/app/store/mask.ts +++ b/app/store/mask.ts @@ -90,6 +90,7 @@ export const useMaskStore = create()( (a, b) => b.id - a.id, ); const config = useAppConfig.getState(); + if (config.hideBuiltinMasks) return userMasks; const buildinMasks = BUILTIN_MASKS.map( (m) => ({ diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 25d8c361f..5a8822fe1 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.8.7" + "version": "2.8.8" }, "tauri": { "allowlist": {