diff --git a/.github/ISSUE_TEMPLATE/反馈问题.md b/.github/ISSUE_TEMPLATE/反馈问题.md index b21442f5..73ad4b2c 100644 --- a/.github/ISSUE_TEMPLATE/反馈问题.md +++ b/.github/ISSUE_TEMPLATE/反馈问题.md @@ -11,9 +11,10 @@ assignees: '' ⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭。 -> 请在下方中括号内输入 x 来表示你已经知晓相关内容。 +请在下方中括号内输入 x 来表示你已经知晓相关内容。 - [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答; - [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。 +- [ ] 我确认已经在 [Vercel 使用教程](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/vercel-cn.md) 中搜索了此次反馈的问题,没有找到解答。 **描述问题** 请在此描述你遇到了什么问题。 diff --git a/README.md b/README.md index 073c22a7..ec11948e 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - Privacy first, all data stored locally in the browser - Responsive design, dark mode and PWA - Fast first screen loading speed (~100kb), support streaming response +- New in v2: create, share and debug your chat tools with prompt templates (mask) - Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) - Automatically compresses chat history to support long conversations while also saving your tokens - One-click export all chat history with full Markdown support @@ -37,7 +38,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138) - [x] User Prompt: user can edit and save custom prompts to prompt list -- [ ] Prompt Template: create a new chat with pre-defined in-context prompts +- [x] Prompt Template: create a new chat with pre-defined in-context prompts [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993) - [ ] Share as image, share to ShareGPT - [ ] Desktop App with tauri - [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. @@ -48,12 +49,16 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - User login, accounts, cloud sync - UI text customize +## What's New +- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/). + ## 主要功能 - 在 1 分钟内使用 Vercel **免费一键部署** - 精心设计的 UI,响应式设计,支持深色模式,支持 PWA - 极快的首屏加载速度(~100kb),支持流式响应 - 隐私安全,所有数据保存在用户浏览器本地 +- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话 - 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) - 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 - 一键导出聊天记录,完整的 Markdown 支持 @@ -63,7 +68,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138) - [x] 允许用户自行编辑内置 Prompt 列表 -- [ ] 提示词模板:使用预制上下文快速定制新对话 +- [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993) - [ ] 分享为图片,分享到 ShareGPT - [ ] 使用 tauri 打包桌面应用 - [ ] 支持自部署的大语言模型 @@ -74,6 +79,11 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - 界面文字自定义 - 用户登录、账号管理、消息云同步 +## 最新动态 +- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。 + + + ## Get Started > [简体中文 > 如何开始使用](./README_CN.md#开始使用) @@ -103,6 +113,8 @@ We recommend that you follow the steps below to re-deploy: ### Enable Automatic Updates +> If you encounter a failure of Upstream Sync execution, please manually sync fork once. + After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour: ![Automatic Updates](./docs/images/enable-actions.jpg) diff --git a/README_CN.md b/README_CN.md index 23dfee85..fda80a6f 100644 --- a/README_CN.md +++ b/README_CN.md @@ -15,16 +15,6 @@ -## 主要功能 - -- 在 1 分钟内使用 Vercel **免费一键部署** -- 精心设计的 UI,响应式设计,支持深色模式 -- 极快的首屏加载速度(~100kb) -- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) -- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 -- 一键导出聊天记录,完整的 Markdown 支持 -- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问 - ## 开始使用 1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys); @@ -43,6 +33,7 @@ - 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。 ### 打开自动更新 +> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次! 当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新: diff --git a/app/api/chat-stream/route.ts b/app/api/chat-stream/route.ts index 22550e39..2775ff06 100644 --- a/app/api/chat-stream/route.ts +++ b/app/api/chat-stream/route.ts @@ -59,4 +59,4 @@ export async function POST(req: NextRequest) { } } -export const runtime = "experimental-edge"; +export const runtime = "edge"; diff --git a/app/api/config/route.ts b/app/api/config/route.ts index e04e22a0..65290a47 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -19,3 +19,5 @@ export async function POST(req: NextRequest) { needCode: serverConfig.needCode, }); } + +export const runtime = "edge"; diff --git a/app/api/openai/route.ts b/app/api/openai/route.ts index bed70d92..d49027c6 100644 --- a/app/api/openai/route.ts +++ b/app/api/openai/route.ts @@ -17,7 +17,7 @@ async function makeRequest(req: NextRequest) { }, { status: 500, - } + }, ); } } @@ -30,4 +30,4 @@ export async function GET(req: NextRequest) { return makeRequest(req); } -export const runtime = "experimental-edge"; +export const runtime = "edge"; diff --git a/app/api/openai/typing.ts b/app/api/openai/typing.ts index b936530c..2286d231 100644 --- a/app/api/openai/typing.ts +++ b/app/api/openai/typing.ts @@ -5,3 +5,5 @@ import type { export type ChatRequest = CreateChatCompletionRequest; export type ChatResponse = CreateChatCompletionResponse; + +export type Updater = (updater: (value: T) => void) => void; diff --git a/app/components/button.module.scss b/app/components/button.module.scss index 3a3393e7..5aa53dcf 100644 --- a/app/components/button.module.scss +++ b/app/components/button.module.scss @@ -18,6 +18,15 @@ cursor: not-allowed; opacity: 0.5; } + + &.primary { + background-color: var(--primary); + color: white; + + path { + fill: white !important; + } + } } .shadow { diff --git a/app/components/button.tsx b/app/components/button.tsx index 1675a4b7..f93741b3 100644 --- a/app/components/button.tsx +++ b/app/components/button.tsx @@ -4,11 +4,11 @@ import styles from "./button.module.scss"; export function IconButton(props: { onClick?: () => void; - icon: JSX.Element; + icon?: JSX.Element; + type?: "primary" | "danger"; text?: string; bordered?: boolean; shadow?: boolean; - noDark?: boolean; className?: string; title?: string; disabled?: boolean; @@ -19,18 +19,24 @@ export function IconButton(props: { styles["icon-button"] + ` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${ props.className ?? "" - } clickable` + } clickable ${styles[props.type ?? ""]}` } onClick={props.onClick} title={props.title} disabled={props.disabled} role="button" > -
- {props.icon} -
+ {props.icon && ( +
+ {props.icon} +
+ )} + {props.text && (
{props.text}
)} diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index 626336af..2c7b95aa 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -1,4 +1,6 @@ import DeleteIcon from "../icons/delete.svg"; +import BotIcon from "../icons/bot.svg"; + import styles from "./home.module.scss"; import { DragDropContext, @@ -12,6 +14,8 @@ import { useChatStore } from "../store"; import Locale from "../locales"; import { Link, useNavigate } from "react-router-dom"; import { Path } from "../constant"; +import { MaskAvatar } from "./mask"; +import { Mask } from "../store/mask"; export function ChatItem(props: { onClick?: () => void; @@ -23,6 +27,7 @@ export function ChatItem(props: { id: number; index: number; narrow?: boolean; + mask: Mask; }) { return ( @@ -35,9 +40,19 @@ export function ChatItem(props: { ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} + title={`${props.title}\n${Locale.ChatItem.ChatItemCount( + props.count, + )}`} > {props.narrow ? ( -
{props.count}
+
+
+ +
+
+ {props.count} +
+
) : ( <>
{props.title}
@@ -45,7 +60,9 @@ export function ChatItem(props: {
{Locale.ChatItem.ChatItemCount(props.count)}
-
{props.time}
+
+ {new Date(props.time).toLocaleString()} +
)} @@ -99,7 +116,7 @@ export function ChatList(props: { narrow?: boolean }) { {sessions.map((item, i) => ( ))} {provided.placeholder} diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 7cd2889f..3a1be391 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -53,6 +53,20 @@ } } +.section-title { + font-size: 12px; + font-weight: bold; + margin-bottom: 10px; + display: flex; + justify-content: space-between; + align-items: center; + + .section-title-action { + display: flex; + align-items: center; + } +} + .context-prompt { .context-prompt-row { display: flex; @@ -81,25 +95,13 @@ } .memory-prompt { - margin-top: 20px; - - .memory-prompt-title { - font-size: 12px; - font-weight: bold; - margin-bottom: 10px; - display: flex; - justify-content: space-between; - align-items: center; - - .memory-prompt-action { - display: flex; - align-items: center; - } - } + margin: 20px 0; .memory-prompt-content { - background-color: var(--gray); - border-radius: 6px; + background-color: var(--white); + color: var(--black); + border: var(--border-in-light); + border-radius: 10px; padding: 10px; font-size: 12px; user-select: text; diff --git a/app/components/chat.tsx b/app/components/chat.tsx index cb163cff..8fc39e31 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1,4 +1,4 @@ -import { useDebounce, useDebouncedCallback } from "use-debounce"; +import { useDebouncedCallback } from "use-debounce"; import { memo, useState, useRef, useEffect, useLayoutEffect } from "react"; import SendWhiteIcon from "../icons/send-white.svg"; @@ -9,12 +9,11 @@ import ReturnIcon from "../icons/return.svg"; import CopyIcon from "../icons/copy.svg"; import DownloadIcon from "../icons/download.svg"; import LoadingIcon from "../icons/three-dots.svg"; -import BotIcon from "../icons/bot.svg"; -import BlackBotIcon from "../icons/black-bot.svg"; -import AddIcon from "../icons/add.svg"; -import DeleteIcon from "../icons/delete.svg"; +import PromptIcon from "../icons/prompt.svg"; +import MaskIcon from "../icons/mask.svg"; import MaxIcon from "../icons/max.svg"; import MinIcon from "../icons/min.svg"; +import ResetIcon from "../icons/reload.svg"; import LightIcon from "../icons/light.svg"; import DarkIcon from "../icons/dark.svg"; @@ -31,14 +30,14 @@ import { createMessage, useAccessStore, Theme, - ModelType, useAppConfig, + ModelConfig, + DEFAULT_TOPIC, } from "../store"; import { copyToClipboard, downloadAs, - getEmojiUrl, selectOrCopy, autoGrowTextArea, useMobileScreen, @@ -54,9 +53,16 @@ import { IconButton } from "./button"; import styles from "./home.module.scss"; import chatStyle from "./chat.module.scss"; -import { Input, Modal, showModal } from "./ui-lib"; +import { ListItem, Modal, showModal } from "./ui-lib"; import { useNavigate } from "react-router-dom"; import { Path } from "../constant"; +import { Avatar } from "./emoji"; +import { MaskAvatar, MaskConfig } from "./mask"; +import { + DEFAULT_MASK_AVATAR, + DEFAULT_MASK_ID, + useMaskStore, +} from "../store/mask"; const Markdown = dynamic( async () => memo((await import("./markdown")).Markdown), @@ -65,32 +71,6 @@ const Markdown = dynamic( }, ); -const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, { - loading: () => , -}); - -export function Avatar(props: { role: Message["role"]; model?: ModelType }) { - const config = useAppConfig(); - - if (props.role !== "user") { - return ( -
- {props.model?.startsWith("gpt-4") ? ( - - ) : ( - - )} -
- ); - } - - return ( -
- -
- ); -} - function exportMessages(messages: Message[], topic: string) { const mdText = `# ${topic}\n\n` + @@ -129,6 +109,64 @@ function exportMessages(messages: Message[], topic: string) { }); } +export function SessionConfigModel(props: { onClose: () => void }) { + const chatStore = useChatStore(); + const session = chatStore.currentSession(); + const maskStore = useMaskStore(); + const navigate = useNavigate(); + + return ( +
+ props.onClose()} + actions={[ + } + bordered + text={Locale.Chat.Config.Reset} + onClick={() => + confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession() + } + />, + } + bordered + text={Locale.Chat.Config.SaveAs} + onClick={() => { + navigate(Path.Masks); + setTimeout(() => { + maskStore.create(session.mask); + }, 500); + }} + />, + ]} + > + { + const mask = { ...session.mask }; + updater(mask); + chatStore.updateCurrentSession((session) => (session.mask = mask)); + }} + extraListItems={ + session.mask.modelConfig.sendMemory ? ( + + ) : ( + <> + ) + } + > + +
+ ); +} + function PromptToast(props: { showToast?: boolean; showModal?: boolean; @@ -136,25 +174,7 @@ function PromptToast(props: { }) { const chatStore = useChatStore(); const session = chatStore.currentSession(); - const context = session.context; - - const addContextPrompt = (prompt: Message) => { - chatStore.updateCurrentSession((session) => { - session.context.push(prompt); - }); - }; - - const removeContextPrompt = (i: number) => { - chatStore.updateCurrentSession((session) => { - session.context.splice(i, 1); - }); - }; - - const updateContextPrompt = (i: number, prompt: Message) => { - chatStore.updateCurrentSession((session) => { - session.context[i] = prompt; - }); - }; + const context = session.mask.context; return (
@@ -171,115 +191,7 @@ function PromptToast(props: {
)} {props.showModal && ( -
- props.setShowModal(false)} - actions={[ - } - bordered - text={Locale.Memory.Reset} - onClick={() => - confirm(Locale.Memory.ResetConfirm) && - chatStore.resetSession() - } - />, - } - bordered - text={Locale.Memory.Copy} - onClick={() => copyToClipboard(session.memoryPrompt)} - />, - ]} - > - <> -
- {context.map((c, i) => ( -
- - - updateContextPrompt(i, { - ...c, - content: e.currentTarget.value as any, - }) - } - /> - } - className={chatStyle["context-delete-button"]} - onClick={() => removeContextPrompt(i)} - bordered - /> -
- ))} - -
- } - text={Locale.Context.Add} - bordered - className={chatStyle["context-prompt-button"]} - onClick={() => - addContextPrompt({ - role: "system", - content: "", - date: "", - }) - } - /> -
-
-
-
- - {Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "} - {session.messages.length}) - - - -
-
- {session.memoryPrompt || Locale.Memory.EmptyContent} -
-
- -
-
+ props.setShowModal(false)} /> )} ); @@ -360,9 +272,11 @@ function useScrollToBottom() { export function ChatActions(props: { showPromptModal: () => void; scrollToBottom: () => void; + showPromptHints: () => void; hitBottom: boolean; }) { const config = useAppConfig(); + const navigate = useNavigate(); // switch themes const theme = config.theme; @@ -417,6 +331,22 @@ export function ChatActions(props: { ) : null} + +
+ +
+ +
{ + navigate(Path.Masks); + }} + > + +
); } @@ -459,9 +389,9 @@ export function Chat() { ); const onPromptSelect = (prompt: Prompt) => { - setUserInput(prompt.content); setPromptHints([]); inputRef.current?.focus(); + setUserInput(prompt.content); }; // auto grow input @@ -585,7 +515,7 @@ export function Chat() { inputRef.current?.focus(); }; - const context: RenderMessage[] = session.context.slice(); + const context: RenderMessage[] = session.mask.context.slice(); const accessStore = useAccessStore(); @@ -648,20 +578,20 @@ export function Chat() { return (
-
-
+
+
- {session.topic} + {!session.topic ? DEFAULT_TOPIC : session.topic}
-
+
{Locale.Chat.SubTitle(session.messages.length)}
-
-
+
+
} bordered @@ -669,14 +599,14 @@ export function Chat() { onClick={() => navigate(Path.Home)} />
-
+
} bordered onClick={renameSession} />
-
+
} bordered @@ -690,7 +620,7 @@ export function Chat() { />
{!isMobileScreen && ( -
+
: } bordered @@ -739,7 +669,11 @@ export function Chat() { >
- + {message.role === "user" ? ( + + ) : ( + + )}
{showTyping && (
@@ -816,6 +750,10 @@ export function Chat() { showPromptModal={() => setShowPromptModal(true)} scrollToBottom={scrollToBottom} hitBottom={hitBottom} + showPromptHints={() => { + inputRef.current?.focus(); + onSearch(""); + }} />
); } + +export function PasswordInput(props: HTMLProps) { + const [visible, setVisible] = useState(false); + + function changeVisibility() { + setVisible(!visible); + } + + return ( +
+ : } + onClick={changeVisibility} + className={"password-eye"} + /> + +
+ ); +} diff --git a/app/constant.ts b/app/constant.ts index 43ae4cc6..e0ec03d0 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -11,6 +11,26 @@ export enum Path { Home = "/", Chat = "/chat", Settings = "/settings", + NewChat = "/new-chat", + Masks = "/masks", +} + +export enum SlotID { + AppBody = "app-body", +} + +export enum FileName { + Masks = "masks.json", + Prompts = "prompts.json", +} + +export enum StoreKey { + Chat = "chat-next-web-store", + Access = "access-control", + Config = "app-config", + Mask = "mask-store", + Prompt = "prompt-store", + Update = "chat-update", } export const MAX_SIDEBAR_WIDTH = 500; diff --git a/app/icons/black-bot.svg b/app/icons/black-bot.svg index 3aad2add..5c253fb0 100644 --- a/app/icons/black-bot.svg +++ b/app/icons/black-bot.svg @@ -18,7 +18,7 @@ - diff --git a/app/icons/dark.svg b/app/icons/dark.svg index 3eebc373..c96c188b 100644 --- a/app/icons/dark.svg +++ b/app/icons/dark.svg @@ -1 +1 @@ - + diff --git a/app/icons/delete.svg b/app/icons/delete.svg index 49ab97b8..b28095b5 100644 --- a/app/icons/delete.svg +++ b/app/icons/delete.svg @@ -7,6 +7,6 @@ fill-rule="evenodd" fill="#D8D8D8" fill-opacity="0.009999999776482582" /> + fill="#333333" fill-opacity="1" /> - \ No newline at end of file + diff --git a/app/icons/left.svg b/app/icons/left.svg new file mode 100644 index 00000000..8f1cf52d --- /dev/null +++ b/app/icons/left.svg @@ -0,0 +1 @@ + diff --git a/app/icons/light.svg b/app/icons/light.svg index 22cfa1ff..a425e010 100644 --- a/app/icons/light.svg +++ b/app/icons/light.svg @@ -1 +1 @@ - + diff --git a/app/icons/lightning.svg b/app/icons/lightning.svg new file mode 100644 index 00000000..d208ad87 --- /dev/null +++ b/app/icons/lightning.svg @@ -0,0 +1 @@ + diff --git a/app/icons/mask.svg b/app/icons/mask.svg new file mode 100644 index 00000000..69b600c5 --- /dev/null +++ b/app/icons/mask.svg @@ -0,0 +1 @@ + diff --git a/app/icons/plugin.svg b/app/icons/plugin.svg new file mode 100644 index 00000000..8da8b0e7 --- /dev/null +++ b/app/icons/plugin.svg @@ -0,0 +1 @@ + diff --git a/app/icons/prompt.svg b/app/icons/prompt.svg new file mode 100644 index 00000000..686f4c17 --- /dev/null +++ b/app/icons/prompt.svg @@ -0,0 +1 @@ + diff --git a/app/icons/upload.svg b/app/icons/upload.svg new file mode 100644 index 00000000..f9999a7f --- /dev/null +++ b/app/icons/upload.svg @@ -0,0 +1 @@ + diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 777cea59..208752c1 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -3,7 +3,8 @@ import { SubmitKey } from "../store/config"; const cn = { WIP: "该功能仍在开发中……", Error: { - Unauthorized: "现在是未授权状态,请点击左下角设置按钮输入访问密码。", + Unauthorized: + "现在是未授权状态,请点击左下角[设置](/#/settings)按钮输入访问密码。", }, ChatItem: { ChatItemCount: (count: number) => `${count} 条对话`, @@ -29,6 +30,10 @@ const cn = { return inputHints + ",/ 触发补全"; }, Send: "发送", + Config: { + Reset: "重置默认", + SaveAs: "另存为面具", + }, }, Export: { Title: "导出聊天记录为 Markdown", @@ -39,8 +44,8 @@ const cn = { }, Memory: { Title: "历史摘要", - EmptyContent: "尚未总结", - Send: "启用总结并发送摘要", + EmptyContent: "对话内容过短,无需总结", + Send: "自动压缩聊天记录并作为上下文发送", Copy: "复制摘要", Reset: "重置对话", ResetConfirm: "重置后将清空当前对话记录以及历史摘要,确认重置?", @@ -58,15 +63,12 @@ const cn = { ClearAll: "清除所有数据", ResetAll: "重置所有选项", Close: "关闭", - ConfirmResetAll: { - Confirm: "确认清除所有配置?", - }, - ConfirmClearAll: { - Confirm: "确认清除所有聊天记录?", - }, + ConfirmResetAll: "确认重置所有配置?", + ConfirmClearAll: "确认清除所有数据?", }, Lang: { Name: "Language", + All: "所有语言", Options: { cn: "简体中文", en: "English", @@ -95,7 +97,14 @@ const cn = { SendKey: "发送键", Theme: "主题", TightBorder: "无边框模式", - SendPreviewBubble: "发送预览气泡", + SendPreviewBubble: { + Title: "预览气泡", + SubTitle: "在预览气泡中预览 Markdown 内容", + }, + Mask: { + Title: "面具启动页", + SubTitle: "新建聊天时,展示面具启动页", + }, Prompt: { Disable: { Title: "禁用提示词自动补全", @@ -124,6 +133,7 @@ const cn = { SubTitle: "使用自己的 Key 可绕过密码访问限制", Placeholder: "OpenAI API Key", }, + Usage: { Title: "余额查询", SubTitle(used: any, total: any) { @@ -141,7 +151,7 @@ const cn = { Model: "模型 (model)", Temperature: { Title: "随机性 (temperature)", - SubTitle: "值越大,回复越随机,大于 1 的值可能会导致乱码", + SubTitle: "值越大,回复越随机", }, MaxTokens: { Title: "单次回复限制 (max_tokens)", @@ -164,7 +174,6 @@ const cn = { Summarize: "简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 200 字以内", }, - ConfirmClearAll: "确认清除所有聊天、设置数据?", }, Copy: { Success: "已写入剪切板", @@ -172,8 +181,47 @@ const cn = { }, Context: { Toast: (x: any) => `已设置 ${x} 条前置上下文`, - Edit: "前置上下文和历史记忆", - Add: "新增一条", + Edit: "当前对话设置", + Add: "新增预设对话", + }, + 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 ? "(只读)" : ""}`, + Download: "下载预设", + Clone: "克隆预设", + }, + Config: { + Avatar: "角色头像", + Name: "角色名称", + }, + }, + NewChat: { + Return: "返回", + Skip: "直接开始", + NotShow: "不再展示", + ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。", + Title: "挑选一个面具", + SubTitle: "现在开始,与面具背后的灵魂思维碰撞", + More: "搜索更多", }, }; diff --git a/app/locales/de.ts b/app/locales/de.ts index 42a4c8f6..1981944f 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -31,6 +31,10 @@ const de: LocaleType = { return inputHints + ", / zum Durchsuchen von Prompts"; }, Send: "Senden", + Config: { + Reset: "Reset to Default", + SaveAs: "Save as Mask", + }, }, Export: { Title: "Alle Nachrichten", @@ -61,15 +65,13 @@ const de: LocaleType = { ClearAll: "Alle Daten löschen", ResetAll: "Alle Einstellungen zurücksetzen", Close: "Schließen", - ConfirmResetAll: { - Confirm: "Möchten Sie wirklich alle Konfigurationen zurücksetzen?", - }, - ConfirmClearAll: { - Confirm: "Möchten Sie wirklich alle Chats zurücksetzen?", - }, + ConfirmResetAll: + "Möchten Sie wirklich alle Konfigurationen zurücksetzen?", + ConfirmClearAll: "Möchten Sie wirklich alle Chats zurücksetzen?", }, Lang: { Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` + All: "All Languages", Options: { cn: "简体中文", en: "English", @@ -97,7 +99,14 @@ const de: LocaleType = { SendKey: "Senden-Taste", Theme: "Erscheinungsbild", TightBorder: "Enger Rahmen", - SendPreviewBubble: "Vorschau-Bubble senden", + SendPreviewBubble: { + Title: "Vorschau-Bubble senden", + SubTitle: "Preview markdown in bubble", + }, + Mask: { + Title: "Mask Splash Screen", + SubTitle: "Show a mask splash screen before starting new chat", + }, Prompt: { Disable: { Title: "Autovervollständigung deaktivieren", @@ -171,8 +180,6 @@ const de: LocaleType = { Summarize: "Fassen Sie unsere Diskussion kurz in 200 Wörtern oder weniger zusammen, um sie als Pronpt für zukünftige Gespräche zu verwenden.", }, - ConfirmClearAll: - "Bestätigen Sie, um alle Chat- und Einstellungsdaten zu löschen?", }, Copy: { Success: "In die Zwischenablage kopiert", @@ -184,6 +191,45 @@ const de: LocaleType = { Edit: "Kontext- und Gedächtnis-Prompts", Add: "Hinzufügen", }, + Plugin: { + Name: "Plugin", + }, + Mask: { + Name: "Mask", + Page: { + Title: "Prompt Template", + SubTitle: (count: number) => `${count} prompt templates`, + Search: "Search Templates", + Create: "Create", + }, + Item: { + Info: (count: number) => `${count} prompts`, + Chat: "Chat", + View: "View", + Edit: "Edit", + Delete: "Delete", + DeleteConfirm: "Confirm to delete?", + }, + EditModal: { + Title: (readonly: boolean) => + `Edit Prompt Template ${readonly ? "(readonly)" : ""}`, + Download: "Download", + Clone: "Clone", + }, + Config: { + Avatar: "Bot Avatar", + Name: "Bot Name", + }, + }, + NewChat: { + Return: "Return", + Skip: "Skip", + Title: "Pick a Mask", + SubTitle: "Chat with the Soul behind the Mask", + More: "Find More", + NotShow: "Not Show Again", + ConfirmNoShow: "Confirm to disable?You can enable it in settings later.", + }, }; export default de; diff --git a/app/locales/en.ts b/app/locales/en.ts index f7af4bfb..d7357731 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -2,7 +2,7 @@ import { SubmitKey } from "../store/config"; import type { LocaleType } from "./index"; const en: LocaleType = { - WIP: "WIP...", + WIP: "Coming Soon...", Error: { Unauthorized: "Unauthorized access, please enter access code in settings page.", @@ -31,6 +31,10 @@ const en: LocaleType = { return inputHints + ", / to search prompts"; }, Send: "Send", + Config: { + Reset: "Reset to Default", + SaveAs: "Save as Mask", + }, }, Export: { Title: "All Messages", @@ -61,15 +65,12 @@ const en: LocaleType = { ClearAll: "Clear All Data", ResetAll: "Reset All Settings", Close: "Close", - ConfirmResetAll: { - Confirm: "Are you sure you want to reset all configurations?", - }, - ConfirmClearAll: { - Confirm: "Are you sure you want to reset all chat?", - }, + ConfirmResetAll: "Are you sure you want to reset all configurations?", + ConfirmClearAll: "Are you sure you want to reset all data?", }, Lang: { Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` + All: "All Languages", Options: { cn: "简体中文", en: "English", @@ -97,7 +98,14 @@ const en: LocaleType = { SendKey: "Send Key", Theme: "Theme", TightBorder: "Tight Border", - SendPreviewBubble: "Send Preview Bubble", + SendPreviewBubble: { + Title: "Send Preview Bubble", + SubTitle: "Preview markdown in bubble", + }, + Mask: { + Title: "Mask Splash Screen", + SubTitle: "Show a mask splash screen before starting new chat", + }, Prompt: { Disable: { Title: "Disable auto-completion", @@ -169,7 +177,6 @@ const en: LocaleType = { Summarize: "Summarize our discussion briefly in 200 words or less to use as a prompt for future context.", }, - ConfirmClearAll: "Confirm to clear all chat and setting data?", }, Copy: { Success: "Copied to clipboard", @@ -178,7 +185,46 @@ const en: LocaleType = { Context: { Toast: (x: any) => `With ${x} contextual prompts`, Edit: "Contextual and Memory Prompts", - Add: "Add One", + Add: "Add a Prompt", + }, + Plugin: { + Name: "Plugin", + }, + Mask: { + Name: "Mask", + Page: { + Title: "Prompt Template", + SubTitle: (count: number) => `${count} prompt templates`, + Search: "Search Templates", + Create: "Create", + }, + Item: { + Info: (count: number) => `${count} prompts`, + Chat: "Chat", + View: "View", + Edit: "Edit", + Delete: "Delete", + DeleteConfirm: "Confirm to delete?", + }, + EditModal: { + Title: (readonly: boolean) => + `Edit Prompt Template ${readonly ? "(readonly)" : ""}`, + Download: "Download", + Clone: "Clone", + }, + Config: { + Avatar: "Bot Avatar", + Name: "Bot Name", + }, + }, + NewChat: { + Return: "Return", + Skip: "Skip", + Title: "Pick a Mask", + SubTitle: "Chat with the Soul behind the Mask", + More: "Find More", + NotShow: "Not Show Again", + ConfirmNoShow: "Confirm to disable?You can enable it in settings later.", }, }; diff --git a/app/locales/es.ts b/app/locales/es.ts index efecf113..783cd6e9 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -31,6 +31,10 @@ const es: LocaleType = { return inputHints; }, Send: "Enviar", + Config: { + Reset: "Reset to Default", + SaveAs: "Save as Mask", + }, }, Export: { Title: "Todos los mensajes", @@ -61,15 +65,12 @@ const es: LocaleType = { ClearAll: "Borrar todos los datos", ResetAll: "Restablecer todas las configuraciones", Close: "Cerrar", - ConfirmResetAll: { - Confirm: "Are you sure you want to reset all configurations?", - }, - ConfirmClearAll: { - Confirm: "Are you sure you want to reset all chat?", - }, + ConfirmResetAll: "Are you sure you want to reset all configurations?", + ConfirmClearAll: "Are you sure you want to reset all chat?", }, Lang: { Name: "Language", + All: "All Languages", Options: { cn: "简体中文", en: "Inglés", @@ -97,7 +98,14 @@ const es: LocaleType = { SendKey: "Tecla de envío", Theme: "Tema", TightBorder: "Borde ajustado", - SendPreviewBubble: "Enviar burbuja de vista previa", + SendPreviewBubble: { + Title: "Enviar burbuja de vista previa", + SubTitle: "Preview markdown in bubble", + }, + Mask: { + Title: "Mask Splash Screen", + SubTitle: "Show a mask splash screen before starting new chat", + }, Prompt: { Disable: { Title: "Desactivar autocompletado", @@ -169,8 +177,6 @@ const es: LocaleType = { Summarize: "Resuma nuestra discusión brevemente en 200 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", @@ -182,6 +188,45 @@ const es: LocaleType = { Edit: "Contextual and Memory Prompts", Add: "Add One", }, + Plugin: { + Name: "Plugin", + }, + Mask: { + Name: "Mask", + Page: { + Title: "Prompt Template", + SubTitle: (count: number) => `${count} prompt templates`, + Search: "Search Templates", + Create: "Create", + }, + Item: { + Info: (count: number) => `${count} prompts`, + Chat: "Chat", + View: "View", + Edit: "Edit", + Delete: "Delete", + DeleteConfirm: "Confirm to delete?", + }, + EditModal: { + Title: (readonly: boolean) => + `Edit Prompt Template ${readonly ? "(readonly)" : ""}`, + Download: "Download", + Clone: "Clone", + }, + Config: { + Avatar: "Bot Avatar", + Name: "Bot Name", + }, + }, + NewChat: { + Return: "Return", + Skip: "Skip", + Title: "Pick a Mask", + SubTitle: "Chat with the Soul behind the Mask", + More: "Find More", + NotShow: "Not Show Again", + ConfirmNoShow: "Confirm to disable?You can enable it in settings later.", + }, }; export default es; diff --git a/app/locales/index.ts b/app/locales/index.ts index 389304f8..2ce59261 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -19,7 +19,7 @@ export const AllLangs = [ "jp", "de", ] as const; -type Lang = (typeof AllLangs)[number]; +export type Lang = (typeof AllLangs)[number]; const LANG_KEY = "lang"; diff --git a/app/locales/it.ts b/app/locales/it.ts index b519ef45..3fcc80ec 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -31,6 +31,10 @@ const it: LocaleType = { return inputHints; }, Send: "Invia", + Config: { + Reset: "Reset to Default", + SaveAs: "Save as Mask", + }, }, Export: { Title: "Tutti i messaggi", @@ -61,15 +65,12 @@ const it: LocaleType = { ClearAll: "Cancella tutti i dati", ResetAll: "Resetta tutte le impostazioni", Close: "Chiudi", - ConfirmResetAll: { - Confirm: "Sei sicuro vuoi cancellare tutte le impostazioni?", - }, - ConfirmClearAll: { - Confirm: "Sei sicuro vuoi cancellare tutte le chat?", - }, + ConfirmResetAll: "Sei sicuro vuoi cancellare tutte le impostazioni?", + ConfirmClearAll: "Sei sicuro vuoi cancellare tutte le chat?", }, Lang: { Name: "Lingue", + All: "All Languages", Options: { cn: "简体中文", en: "English", @@ -97,7 +98,14 @@ const it: LocaleType = { SendKey: "Tasto invia", Theme: "Tema", TightBorder: "Schermo intero", - SendPreviewBubble: "Anteprima di digitazione", + SendPreviewBubble: { + Title: "Anteprima di digitazione", + SubTitle: "Preview markdown in bubble", + }, + Mask: { + Title: "Mask Splash Screen", + SubTitle: "Show a mask splash screen before starting new chat", + }, Prompt: { Disable: { Title: "Disabilita l'auto completamento", @@ -170,8 +178,6 @@ const it: LocaleType = { Summarize: "Riassumi brevemente la nostra discussione in 200 caratteri o meno per usarla come spunto per una futura conversazione.", }, - ConfirmClearAll: - "Confermi la cancellazione di tutti i dati della chat e delle impostazioni?", }, Copy: { Success: "Copiato sugli appunti", @@ -183,6 +189,45 @@ const it: LocaleType = { Edit: "Prompt contestuali e di memoria", Add: "Aggiungi altro", }, + Plugin: { + Name: "Plugin", + }, + Mask: { + Name: "Mask", + Page: { + Title: "Prompt Template", + SubTitle: (count: number) => `${count} prompt templates`, + Search: "Search Templates", + Create: "Create", + }, + Item: { + Info: (count: number) => `${count} prompts`, + Chat: "Chat", + View: "View", + Edit: "Edit", + Delete: "Delete", + DeleteConfirm: "Confirm to delete?", + }, + EditModal: { + Title: (readonly: boolean) => + `Edit Prompt Template ${readonly ? "(readonly)" : ""}`, + Download: "Download", + Clone: "Clone", + }, + Config: { + Avatar: "Bot Avatar", + Name: "Bot Name", + }, + }, + NewChat: { + Return: "Return", + Skip: "Skip", + Title: "Pick a Mask", + SubTitle: "Chat with the Soul behind the Mask", + More: "Find More", + NotShow: "Not Show Again", + ConfirmNoShow: "Confirm to disable?You can enable it in settings later.", + }, }; export default it; diff --git a/app/locales/jp.ts b/app/locales/jp.ts index 1c8d66d9..4bbe0512 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -1,6 +1,7 @@ import { SubmitKey } from "../store/config"; +import type { LocaleType } from "./index"; -const jp = { +const jp: LocaleType = { WIP: "この機能は開発中です……", Error: { Unauthorized: @@ -30,6 +31,10 @@ const jp = { return inputHints + ",/ で自動補完をトリガー"; }, Send: "送信", + Config: { + Reset: "重置默认", + SaveAs: "另存为面具", + }, }, Export: { Title: "チャット履歴をMarkdown形式でエクスポート", @@ -60,15 +65,12 @@ const jp = { ClearAll: "すべてのデータをクリア", ResetAll: "すべてのオプションをリセット", Close: "閉じる", - ConfirmResetAll: { - Confirm: "すべての設定をリセットしてもよろしいですか?", - }, - ConfirmClearAll: { - Confirm: "すべてのチャットをリセットしてもよろしいですか?", - }, + ConfirmResetAll: "すべての設定をリセットしてもよろしいですか?", + ConfirmClearAll: "すべてのチャットをリセットしてもよろしいですか?", }, Lang: { Name: "Language", + All: "所有语言", Options: { cn: "简体中文", en: "English", @@ -97,7 +99,14 @@ const jp = { SendKey: "送信キー", Theme: "テーマ", TightBorder: "ボーダーレスモード", - SendPreviewBubble: "プレビューバブルの送信", + SendPreviewBubble: { + Title: "プレビューバブルの送信", + SubTitle: "在预览气泡中预览 Markdown 内容", + }, + Mask: { + Title: "面具启动页", + SubTitle: "新建聊天时,展示面具启动页", + }, Prompt: { Disable: { Title: "プロンプトの自動補完を無効にする", @@ -170,8 +179,6 @@ const jp = { Summarize: "あなたとユーザの会話を簡潔にまとめて、後続のコンテキストプロンプトとして使ってください。200字以内に抑えてください。", }, - ConfirmClearAll: - "すべてのチャット、設定データをクリアしてもよろしいですか?", }, Copy: { Success: "クリップボードに書き込みました", @@ -182,6 +189,43 @@ const jp = { Edit: "前置コンテキストと履歴メモリ", Add: "新規追加", }, + 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 ? "(只读)" : ""}`, + Download: "下载预设", + Clone: "克隆预设", + }, + Config: { + Avatar: "角色头像", + Name: "角色名称", + }, + }, + NewChat: { + Return: "返回", + Skip: "跳过", + Title: "挑选一个面具", + SubTitle: "现在开始,与面具背后的灵魂思维碰撞", + More: "搜索更多", + NotShow: "不再展示", + ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。", + }, }; export default jp; diff --git a/app/locales/tr.ts b/app/locales/tr.ts index 86f1f417..a658fcc4 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -31,6 +31,10 @@ const tr: LocaleType = { return inputHints + ", komutları aramak için / (eğik çizgi)"; }, Send: "Gönder", + Config: { + Reset: "Reset to Default", + SaveAs: "Save as Mask", + }, }, Export: { Title: "Tüm Mesajlar", @@ -61,15 +65,12 @@ const tr: LocaleType = { ClearAll: "Tüm Verileri Temizle", ResetAll: "Tüm Ayarları Sıfırla", Close: "Kapat", - ConfirmResetAll: { - Confirm: "Tüm ayarları sıfırlamak istediğinizden emin misiniz?", - }, - ConfirmClearAll: { - Confirm: "Tüm sohbeti sıfırlamak istediğinizden emin misiniz?", - }, + ConfirmResetAll: "Tüm ayarları sıfırlamak istediğinizden emin misiniz?", + ConfirmClearAll: "Tüm sohbeti sıfırlamak istediğinizden emin misiniz?", }, Lang: { Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` + All: "All Languages", Options: { cn: "简体中文", en: "English", @@ -97,7 +98,14 @@ const tr: LocaleType = { SendKey: "Gönder Tuşu", Theme: "Tema", TightBorder: "Tam Ekran", - SendPreviewBubble: "Mesaj Önizleme Balonu", + SendPreviewBubble: { + Title: "Mesaj Önizleme Balonu", + SubTitle: "Preview markdown in bubble", + }, + Mask: { + Title: "Mask Splash Screen", + SubTitle: "Show a mask splash screen before starting new chat", + }, Prompt: { Disable: { Title: "Otomatik tamamlamayı devre dışı bırak", @@ -171,8 +179,6 @@ const tr: LocaleType = { Summarize: "Gelecekteki bağlam için bir bilgi istemi olarak kullanmak üzere tartışmamızı en fazla 200 kelimeyle özetleyin.", }, - ConfirmClearAll: - "Tüm sohbet ve ayar verilerini temizlemeyi onaylıyor musunuz?", }, Copy: { Success: "Panoya kopyalandı", @@ -183,6 +189,45 @@ const tr: LocaleType = { Edit: "Bağlamsal ve Bellek Komutları", Add: "Yeni Ekle", }, + Plugin: { + Name: "Plugin", + }, + Mask: { + Name: "Mask", + Page: { + Title: "Prompt Template", + SubTitle: (count: number) => `${count} prompt templates`, + Search: "Search Templates", + Create: "Create", + }, + Item: { + Info: (count: number) => `${count} prompts`, + Chat: "Chat", + View: "View", + Edit: "Edit", + Delete: "Delete", + DeleteConfirm: "Confirm to delete?", + }, + EditModal: { + Title: (readonly: boolean) => + `Edit Prompt Template ${readonly ? "(readonly)" : ""}`, + Download: "Download", + Clone: "Clone", + }, + Config: { + Avatar: "Bot Avatar", + Name: "Bot Name", + }, + }, + NewChat: { + Return: "Return", + Skip: "Skip", + Title: "Pick a Mask", + SubTitle: "Chat with the Soul behind the Mask", + More: "Find More", + NotShow: "Not Show Again", + ConfirmNoShow: "Confirm to disable?You can enable it in settings later.", + }, }; export default tr; diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 26791c77..9a0e9eac 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -30,6 +30,10 @@ const tw: LocaleType = { return inputHints; }, Send: "發送", + Config: { + Reset: "重置默认", + SaveAs: "另存为面具", + }, }, Export: { Title: "將聊天記錄匯出為 Markdown", @@ -59,15 +63,12 @@ const tw: LocaleType = { ClearAll: "清除所有資料", ResetAll: "重設所有設定", Close: "關閉", - ConfirmResetAll: { - Confirm: "您確定要重設所有設定嗎?", - }, - ConfirmClearAll: { - Confirm: "您確定要清除所有聊天嗎?", - }, + ConfirmResetAll: "您確定要重設所有設定嗎?", + ConfirmClearAll: "您確定要清除所有数据嗎?", }, Lang: { - Name: "語言", + Name: "Language", + All: "所有语言", Options: { cn: "简体中文", en: "English", @@ -95,7 +96,14 @@ const tw: LocaleType = { SendKey: "發送鍵", Theme: "主題", TightBorder: "緊湊邊框", - SendPreviewBubble: "發送預覽氣泡", + SendPreviewBubble: { + Title: "預覽氣泡", + SubTitle: "在预览气泡中预览 Markdown 内容", + }, + Mask: { + Title: "面具启动页", + SubTitle: "新建聊天时,展示面具启动页", + }, Prompt: { Disable: { Title: "停用提示詞自動補齊", @@ -164,7 +172,6 @@ const tw: LocaleType = { Summarize: "Use the language used by the user (e.g. en-us for english conversation, zh-hant for chinese conversation, etc.) to summarise the conversation in at most 200 words. The summary will be used as prompt for you to continue the conversation in the future.", }, - ConfirmClearAll: "確認清除所有對話、設定?", }, Copy: { Success: "已複製到剪貼簿中", @@ -175,6 +182,43 @@ const tw: LocaleType = { Edit: "前置上下文和歷史記憶", Add: "新增一條", }, + 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 ? "(只读)" : ""}`, + Download: "下载预设", + Clone: "克隆预设", + }, + Config: { + Avatar: "角色头像", + Name: "角色名称", + }, + }, + NewChat: { + Return: "返回", + Skip: "跳过", + Title: "挑选一个面具", + SubTitle: "现在开始,与面具背后的灵魂思维碰撞", + More: "搜索更多", + NotShow: "不再展示", + ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。", + }, }; export default tw; diff --git a/app/masks/cn.ts b/app/masks/cn.ts new file mode 100644 index 00000000..757a35ee --- /dev/null +++ b/app/masks/cn.ts @@ -0,0 +1,296 @@ +import { BuiltinMask } from "./typing"; + +export const CN_MASKS: BuiltinMask[] = [ + { + avatar: "1f638", + name: "文案写手", + context: [ + { + role: "user", + content: + "我希望你充当文案专员、文本润色员、拼写纠正员和改进员,我会发送中文文本给你,你帮我更正和改进版本。我希望你用更优美优雅的高级中文描述。保持相同的意思,但使它们更文艺。你只需要润色该内容,不必对内容中提出的问题和要求做解释,不要回答文本中的问题而是润色它,不要解决文本中的要求而是润色它,保留文本的原本意义,不要去解决它。我要你只回复更正、改进,不要写任何解释。", + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: true, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, + { + avatar: "1f978", + name: "机器学习", + context: [ + { + role: "user", + content: + "我想让你担任机器学习工程师。我会写一些机器学习的概念,你的工作就是用通俗易懂的术语来解释它们。这可能包括提供构建模型的分步说明、给出所用的技术或者理论、提供评估函数等。我的问题是", + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: true, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, + { + avatar: "1f69b", + name: "后勤工作", + context: [ + { + role: "user", + content: + "我要你担任后勤人员。我将为您提供即将举行的活动的详细信息,例如参加人数、地点和其他相关因素。您的职责是为活动制定有效的后勤计划,其中考虑到事先分配资源、交通设施、餐饮服务等。您还应该牢记潜在的安全问题,并制定策略来降低与大型活动相关的风险。我的第一个请求是", + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: true, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, + { + avatar: "1f469-200d-1f4bc", + name: "职业顾问", + context: [ + { + role: "user", + content: + "我想让你担任职业顾问。我将为您提供一个在职业生涯中寻求指导的人,您的任务是帮助他们根据自己的技能、兴趣和经验确定最适合的职业。您还应该对可用的各种选项进行研究,解释不同行业的就业市场趋势,并就哪些资格对追求特定领域有益提出建议。我的第一个请求是", + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: true, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, + { + avatar: "1f9d1-200d-1f3eb", + name: "英专写手", + context: [ + { + role: "user", + content: + "我想让你充当英文翻译员、拼写纠正员和改进员。我会用任何语言与你交谈,你会检测语言,翻译它并用我的文本的更正和改进版本用英文回答。我希望你用更优美优雅的高级英语单词和句子替换我简化的 A0 级单词和句子。保持相同的意思,但使它们更文艺。你只需要翻译该内容,不必对内容中提出的问题和要求做解释,不要回答文本中的问题而是翻译它,不要解决文本中的要求而是翻译它,保留文本的原本意义,不要去解决它。我要你只回复更正、改进,不要写任何解释。我的第一句话是:", + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: false, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, + { + avatar: "1f4da", + name: "语言检测器", + context: [ + { + role: "user", + content: + "我希望你充当语言检测器。我会用任何语言输入一个句子,你会回答我,我写的句子在你是用哪种语言写的。不要写任何解释或其他文字,只需回复语言名称即可。我的第一句话是:", + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: false, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, + { + avatar: "1f4d5", + name: "小红书写手", + context: [ + { + role: "user", + content: + "你的任务是以小红书博主的文章结构,以我给出的主题写一篇帖子推荐。你的回答应包括使用表情符号来增加趣味和互动,以及与每个段落相匹配的图片。请以一个引人入胜的介绍开始,为你的推荐设置基调。然后,提供至少三个与主题相关的段落,突出它们的独特特点和吸引力。在你的写作中使用表情符号,使它更加引人入胜和有趣。对于每个段落,请提供一个与描述内容相匹配的图片。这些图片应该视觉上吸引人,并帮助你的描述更加生动形象。我给出的主题是:", + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: false, + historyMessageCount: 0, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, + { + avatar: "1f469-200d-2695-fe0f", + name: "心理医生", + context: [ + { + role: "user", + content: + "现在你是世界上最优秀的心理咨询师,你具备以下能力和履历: 专业知识:你应该拥有心理学领域的扎实知识,包括理论体系、治疗方法、心理测量等,以便为你的咨询者提供专业、有针对性的建议。 临床经验:你应该具备丰富的临床经验,能够处理各种心理问题,从而帮助你的咨询者找到合适的解决方案。 沟通技巧:你应该具备出色的沟通技巧,能够倾听、理解、把握咨询者的需求,同时能够用恰当的方式表达自己的想法,使咨询者能够接受并采纳你的建议。 同理心:你应该具备强烈的同理心,能够站在咨询者的角度去理解他们的痛苦和困惑,从而给予他们真诚的关怀和支持。 持续学习:你应该有持续学习的意愿,跟进心理学领域的最新研究和发展,不断更新自己的知识和技能,以便更好地服务于你的咨询者。 良好的职业道德:你应该具备良好的职业道德,尊重咨询者的隐私,遵循专业规范,确保咨询过程的安全和有效性。 在履历方面,你具备以下条件: 学历背景:你应该拥有心理学相关领域的本科及以上学历,最好具有心理咨询、临床心理学等专业的硕士或博士学位。 专业资格:你应该具备相关的心理咨询师执业资格证书,如注册心理师、临床心理师等。 工作经历:你应该拥有多年的心理咨询工作经验,最好在不同类型的心理咨询机构、诊所或医院积累了丰富的实践经验。", + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: true, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, + { + avatar: "1f4b8", + name: "创业点子王", + context: [ + { + role: "user", + content: + "在企业 B2B SaaS 领域中想 3 个创业点子。创业点子应该有一个强大而引人注目的使命,并以某种方式使用人工智能。避免使用加密货币或区块链。创业点子应该有一个很酷很有趣的名字。这些想法应该足够引人注目,这样投资者才会兴奋地投资数百万美元。", + date: "", + }, + { + role: "assistant", + content: + "1. VantageAI - 一个基于人工智能的企业智能平台,帮助中小企业利用数据分析和机器学习来优化其业务流程,提高生产效率并实现可持续发展。\n\n2. HoloLogix - 一个全新的日志处理平台,使用人工智能技术来分析和识别分散的数据源。它可以精确地分析和解释您的日志,从而与整个组织共享并提高数据可视化和分析效率。\n\n3. SmartPath - 一种基于数据的销售和营销自动化平台,可以理解买家的购买行为并根据这些行为提供最佳的营销计划和过程。该平台可以与Salesforce等其他外部工具整合,以更好地掌握您的客户关系管理。", + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: false, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, + { + avatar: "270d-fe0f", + name: "互联网写手", + context: [ + { + role: "user", + content: + "你是一个专业的互联网文章作者,擅长互联网技术介绍、互联网商业、技术应用等方面的写作。\n接下来你要根据用户给你的主题,拓展生成用户想要的文字内容,内容可能是一篇文章、一个开头、一段介绍文字、文章总结、文章结尾等等。\n要求语言通俗易懂、幽默有趣,并且要以第一人称的口吻。", + date: "", + }, + { + role: "assistant", + content: + "好的,我是一名专业的互联网文章作者,非常擅长撰写有关互联网技术介绍、商业应用和技术趋势等方面的内容。只需提供您感兴趣的主题,我就可以为您撰写出一篇生动有趣、通俗易懂的文章。如果遇到不认识的技术名词,我会尽力查询相关知识并告诉您。让我们开始吧!", + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: false, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, + { + avatar: "1f63e", + name: "心灵导师", + context: [ + { + role: "user", + content: + "从现在起你是一个充满哲学思维的心灵导师,当我每次输入一个疑问时你需要用一句富有哲理的名言警句来回答我,并且表明作者和出处\n\n\n要求字数不少于15个字,不超过30字,每次只返回一句且不输出额外的其他信息,你需要使用中文和英文双语输出\n\n\n当你准备好的时候只需要回复“我已经准备好了”(不需要输出任何其他内容)", + date: "", + }, + { + role: "assistant", + content: "我已经准备好了。", + date: "", + }, + { + role: "user", + content: "我作业写不完怎么办", + date: "", + }, + { + role: "assistant", + content: + '“行动是治愈恐惧的良药。” - 威廉·詹姆斯\n"Action is the antidote to fear." - William James', + date: "", + }, + { + role: "user", + content: "论文被拒了怎么办", + date: "", + }, + { + role: "assistant", + content: + '"失败是成功之母。" - 俗语\n"Failure is the mother of success." - Chinese proverb', + date: "", + }, + ], + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: false, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "cn", + builtin: true, + }, +]; diff --git a/app/masks/en.ts b/app/masks/en.ts new file mode 100644 index 00000000..93e9bd6a --- /dev/null +++ b/app/masks/en.ts @@ -0,0 +1,44 @@ +import { BuiltinMask } from "./typing"; + +export const EN_MASKS: BuiltinMask[] = [ + { + avatar: "1f916", + name: "Prompt Improvement", + context: [ + { + role: "user", + content: + 'Read all of the instructions below and once you understand them say "Shall we begin:"\n \nI want you to become my Prompt Creator. Your goal is to help me craft the best possible prompt for my needs. The prompt will be used by you, ChatGPT. You will follow the following process:\nYour first response will be to ask me what the prompt should be about. I will provide my answer, but we will need to improve it through continual iterations by going through the next steps.\n \nBased on my input, you will generate 3 sections.\n \nRevised Prompt (provide your rewritten prompt. it should be clear, concise, and easily understood by you)\nSuggestions (provide 3 suggestions on what details to include in the prompt to improve it)\nQuestions (ask the 3 most relevant questions pertaining to what additional information is needed from me to improve the prompt)\n \nAt the end of these sections give me a reminder of my options which are:\n \nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session\n \nIf I type "Option 2", "2" or "Use this prompt" then we have finsihed and you should use the Revised Prompt as a prompt to generate my request\nIf I type "option 3", "3" or "Restart" then forget the latest Revised Prompt and restart this process\nIf I type "Option 4", "4" or "Quit" then finish this process and revert back to your general mode of operation\n\n\nWe will continue this iterative process with me providing additional information to you and you updating the prompt in the Revised Prompt section until it is complete.', + date: "", + }, + { + role: "assistant", + content: "Shall we begin?", + date: "", + }, + { + role: "user", + content: + "My prompt:I want you to act like Harry Potter from Harry Potter Series. I want you to respond and answer like Harry Potter using the tone, manner and vocabulary Harry Potter would use.", + date: "", + }, + { + role: "assistant", + content: + 'Revised Prompt:\n\n\nWrite a short story or dialogue in which Harry Potter interacts with someone (a friend or foe) using the tone, manner and vocabulary characteristic of his character in the Harry Potter series.\n\n\nSuggestions:\n\n\n\nInclude specific references to Hogwarts, magic spells, and magical creatures to create an authentic Harry Potter experience.\n\nEnsure that Harry\'s personality traits, such as his bravery, loyalty, and determination, are prominently displayed in the story or dialogue.\n\nIncorporate some humor or wit into the story, as these elements are often present in the Harry Potter series.\n\n\nQuestions:\n\n\n\nWho will be the other character interacting with Harry?\n\nWhat is the main conflict or goal of the story/dialogue?\n\nWhat setting or location would you like the story/dialogue to take place in?\n\n\nRemember, your options are:\nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session', + date: "", + }, + ], + modelConfig: { + model: "gpt-4", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + sendMemory: true, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, + }, + lang: "en", + builtin: true, + }, +]; diff --git a/app/masks/index.ts b/app/masks/index.ts new file mode 100644 index 00000000..ea0bf32b --- /dev/null +++ b/app/masks/index.ts @@ -0,0 +1,26 @@ +import { Mask } from "../store/mask"; +import { CN_MASKS } from "./cn"; +import { EN_MASKS } from "./en"; + +import { type BuiltinMask } from "./typing"; +export { type BuiltinMask } from "./typing"; + +export const BUILTIN_MASK_ID = 100000; + +export const BUILTIN_MASK_STORE = { + buildinId: BUILTIN_MASK_ID, + masks: {} as Record, + get(id?: number) { + if (!id) return undefined; + return this.masks[id] as Mask | undefined; + }, + add(m: BuiltinMask) { + const mask = { ...m, id: this.buildinId++ }; + this.masks[mask.id] = mask; + return mask; + }, +}; + +export const BUILTIN_MASKS: Mask[] = [...CN_MASKS, ...EN_MASKS].map((m) => + BUILTIN_MASK_STORE.add(m), +); diff --git a/app/masks/typing.ts b/app/masks/typing.ts new file mode 100644 index 00000000..5f39ccdc --- /dev/null +++ b/app/masks/typing.ts @@ -0,0 +1,3 @@ +import { type Mask } from "../store/mask"; + +export type BuiltinMask = Omit; diff --git a/app/requests.ts b/app/requests.ts index 0e757090..7e92cc45 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -28,11 +28,10 @@ const makeRequestParam = ( sendMessages = sendMessages.filter((m) => m.role !== "assistant"); } - const modelConfig = { ...useAppConfig.getState().modelConfig }; - - // @yidadaa: wont send max_tokens, because it is nonsense for Muggles - // @ts-expect-error - delete modelConfig.max_tokens; + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + }; // override model config if (options?.model) { @@ -42,7 +41,9 @@ const makeRequestParam = ( return { messages: sendMessages, stream: options?.stream, - ...modelConfig, + model: modelConfig.model, + temperature: modelConfig.temperature, + presence_penalty: modelConfig.presence_penalty, }; }; diff --git a/app/store/access.ts b/app/store/access.ts index aed13168..e72052b4 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { StoreKey } from "../constant"; export interface AccessControlStore { accessCode: string; @@ -14,8 +15,6 @@ export interface AccessControlStore { fetch: () => void; } -export const ACCESS_KEY = "access-control"; - let fetchState = 0; // 0 not fetch, 1 fetching, 2 done export const useAccessStore = create()( @@ -62,7 +61,7 @@ export const useAccessStore = create()( }, }), { - name: ACCESS_KEY, + name: StoreKey.Access, version: 1, }, ), diff --git a/app/store/app.ts b/app/store/chat.ts similarity index 84% rename from app/store/app.ts rename to app/store/chat.ts index 2294130a..5607052b 100644 --- a/app/store/app.ts +++ b/app/store/chat.ts @@ -11,7 +11,9 @@ import { isMobileScreen, trimTopic } from "../utils"; import Locale from "../locales"; import { showToast } from "../components/ui-lib"; -import { ModelType, useAppConfig } from "./config"; +import { DEFAULT_CONFIG, ModelConfig, ModelType, useAppConfig } from "./config"; +import { createEmptyMask, Mask } from "./mask"; +import { StoreKey } from "../constant"; export type Message = ChatCompletionResponseMessage & { date: string; @@ -41,50 +43,50 @@ export interface ChatStat { export interface ChatSession { id: number; + topic: string; - sendMemory: boolean; + memoryPrompt: string; - context: Message[]; messages: Message[]; stat: ChatStat; - lastUpdate: string; + lastUpdate: number; lastSummarizeIndex: number; + + mask: Mask; } -const DEFAULT_TOPIC = Locale.Store.DefaultTopic; +export const DEFAULT_TOPIC = Locale.Store.DefaultTopic; export const BOT_HELLO: Message = createMessage({ role: "assistant", content: Locale.Store.BotHello, }); function createEmptySession(): ChatSession { - const createDate = new Date().toLocaleString(); - return { id: Date.now(), topic: DEFAULT_TOPIC, - sendMemory: true, memoryPrompt: "", - context: [], messages: [], stat: { tokenCount: 0, wordCount: 0, charCount: 0, }, - lastUpdate: createDate, + lastUpdate: Date.now(), lastSummarizeIndex: 0, + mask: createEmptyMask(), }; } interface ChatStore { sessions: ChatSession[]; currentSessionIndex: number; + globalId: number; clearSessions: () => void; removeSession: (index: number) => void; moveSession: (from: number, to: number) => void; selectSession: (index: number) => void; - newSession: () => void; + newSession: (mask?: Mask) => void; deleteSession: (index?: number) => void; currentSession: () => ChatSession; onNewMessage: (message: Message) => void; @@ -108,13 +110,12 @@ function countMessages(msgs: Message[]) { return msgs.reduce((pre, cur) => pre + cur.content.length, 0); } -const LOCAL_KEY = "chat-next-web-store"; - export const useChatStore = create()( persist( (set, get) => ({ sessions: [createEmptySession()], currentSessionIndex: 0, + globalId: 0, clearSessions() { set(() => ({ @@ -179,10 +180,20 @@ export const useChatStore = create()( }); }, - newSession() { + newSession(mask) { + const session = createEmptySession(); + + set(() => ({ globalId: get().globalId + 1 })); + session.id = get().globalId; + + if (mask) { + session.mask = { ...mask }; + session.topic = mask.name; + } + set((state) => ({ currentSessionIndex: 0, - sessions: [createEmptySession()].concat(state.sessions), + sessions: [session].concat(state.sessions), })); }, @@ -229,7 +240,7 @@ export const useChatStore = create()( onNewMessage(message) { get().updateCurrentSession((session) => { - session.lastUpdate = new Date().toLocaleString(); + session.lastUpdate = Date.now(); }); get().updateStat(message); get().summarizeSession(); @@ -308,7 +319,10 @@ export const useChatStore = create()( return { role: "system", - content: Locale.Store.Prompt.History(session.memoryPrompt), + content: + session.memoryPrompt.length > 0 + ? Locale.Store.Prompt.History(session.memoryPrompt) + : "", date: "", } as Message; }, @@ -319,11 +333,11 @@ export const useChatStore = create()( const messages = session.messages.filter((msg) => !msg.isError); const n = messages.length; - const context = session.context.slice(); + const context = session.mask.context.slice(); // long term memory if ( - session.sendMemory && + session.mask.modelConfig.sendMemory && session.memoryPrompt && session.memoryPrompt.length > 0 ) { @@ -334,14 +348,14 @@ export const useChatStore = create()( // get short term and unmemoried long term memory const shortTermMemoryMessageIndex = Math.max( 0, - n - config.historyMessageCount, + n - config.modelConfig.historyMessageCount, ); const longTermMemoryMessageIndex = session.lastSummarizeIndex; const oldestIndex = Math.max( shortTermMemoryMessageIndex, longTermMemoryMessageIndex, ); - const threshold = config.compressMessageLengthThreshold; + const threshold = config.modelConfig.compressMessageLengthThreshold; // get recent messages as many as possible const reversedRecentMessages = []; @@ -410,7 +424,7 @@ export const useChatStore = create()( if (historyMsgLength > config?.modelConfig?.max_tokens ?? 4000) { const n = toBeSummarizedMsgs.length; toBeSummarizedMsgs = toBeSummarizedMsgs.slice( - Math.max(0, n - config.historyMessageCount), + Math.max(0, n - config.modelConfig.historyMessageCount), ); } @@ -423,12 +437,13 @@ export const useChatStore = create()( "[Chat History] ", toBeSummarizedMsgs, historyMsgLength, - config.compressMessageLengthThreshold, + config.modelConfig.compressMessageLengthThreshold, ); if ( - historyMsgLength > config.compressMessageLengthThreshold && - session.sendMemory + historyMsgLength > + config.modelConfig.compressMessageLengthThreshold && + session.mask.modelConfig.sendMemory ) { requestChatStream( toBeSummarizedMsgs.concat({ @@ -469,27 +484,34 @@ export const useChatStore = create()( }, clearAllData() { - if (confirm(Locale.Store.ConfirmClearAll)) { - localStorage.clear(); - location.reload(); - } + localStorage.clear(); + location.reload(); }, }), { - name: LOCAL_KEY, - version: 1.2, + name: StoreKey.Chat, + version: 2, migrate(persistedState, version) { - const state = persistedState as ChatStore; + const state = persistedState as any; + const newState = JSON.parse(JSON.stringify(state)) as ChatStore; - if (version === 1) { - state.sessions.forEach((s) => (s.context = [])); + if (version < 2) { + newState.globalId = 0; + newState.sessions = []; + + const oldSessions = state.sessions; + for (const oldSession of oldSessions) { + const newSession = createEmptySession(); + newSession.topic = oldSession.topic; + newSession.messages = [...oldSession.messages]; + newSession.mask.modelConfig.sendMemory = true; + newSession.mask.modelConfig.historyMessageCount = 4; + newSession.mask.modelConfig.compressMessageLengthThreshold = 1000; + newState.sessions.push(newSession); + } } - if (version < 1.2) { - state.sessions.forEach((s) => (s.sendMemory = true)); - } - - return state; + return newState; }, }, ), diff --git a/app/store/config.ts b/app/store/config.ts index 346f38da..4bd68b73 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { StoreKey } from "../constant"; export enum SubmitKey { Enter = "Enter", @@ -15,9 +16,7 @@ export enum Theme { Light = "light", } -const DEFAULT_CONFIG = { - historyMessageCount: 4, - compressMessageLengthThreshold: 1000, +export const DEFAULT_CONFIG = { sendBotMessages: true as boolean, submitKey: SubmitKey.CtrlEnter as SubmitKey, avatar: "1f603", @@ -29,11 +28,16 @@ const DEFAULT_CONFIG = { disablePromptHint: false, + dontShowMaskSplashScreen: false, // dont show splash screen when create chat + modelConfig: { model: "gpt-3.5-turbo" as ModelType, temperature: 1, max_tokens: 2000, presence_penalty: 0, + sendMemory: true, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, }, }; @@ -107,12 +111,10 @@ export const ModalConfigValidator = { return limitNumber(x, -2, 2, 0); }, temperature(x: number) { - return limitNumber(x, 0, 2, 1); + return limitNumber(x, 0, 1, 1); }, }; -const CONFIG_KEY = "app-config"; - export const useAppConfig = create()( persist( (set, get) => ({ @@ -129,7 +131,19 @@ export const useAppConfig = create()( }, }), { - name: CONFIG_KEY, + name: StoreKey.Config, + version: 2, + migrate(persistedState, version) { + if (version === 2) return persistedState as any; + + const state = persistedState as ChatConfig; + state.modelConfig.sendMemory = true; + state.modelConfig.historyMessageCount = 4; + state.modelConfig.compressMessageLengthThreshold = 1000; + state.dontShowMaskSplashScreen = false; + + return state; + }, }, ), ); diff --git a/app/store/index.ts b/app/store/index.ts index 7b7bbd04..0760f48c 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -1,4 +1,4 @@ -export * from "./app"; +export * from "./chat"; export * from "./update"; export * from "./access"; export * from "./config"; diff --git a/app/store/mask.ts b/app/store/mask.ts new file mode 100644 index 00000000..b35e160c --- /dev/null +++ b/app/store/mask.ts @@ -0,0 +1,99 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { BUILTIN_MASKS } from "../masks"; +import { getLang, Lang } from "../locales"; +import { DEFAULT_TOPIC, Message } from "./chat"; +import { ModelConfig, ModelType, useAppConfig } from "./config"; +import { StoreKey } from "../constant"; + +export type Mask = { + id: number; + avatar: string; + name: string; + context: Message[]; + modelConfig: ModelConfig; + lang: Lang; + builtin: boolean; +}; + +export const DEFAULT_MASK_STATE = { + masks: {} as Record, + globalMaskId: 0, +}; + +export type MaskState = typeof DEFAULT_MASK_STATE; +type MaskStore = MaskState & { + create: (mask?: Partial) => Mask; + update: (id: number, updater: (mask: Mask) => void) => void; + delete: (id: number) => void; + search: (text: string) => Mask[]; + get: (id?: number) => Mask | null; + getAll: () => Mask[]; +}; + +export const DEFAULT_MASK_ID = 1145141919810; +export const DEFAULT_MASK_AVATAR = "gpt-bot"; +export const createEmptyMask = () => + ({ + id: DEFAULT_MASK_ID, + avatar: DEFAULT_MASK_AVATAR, + name: DEFAULT_TOPIC, + context: [], + modelConfig: { ...useAppConfig.getState().modelConfig }, + lang: getLang(), + builtin: false, + } as Mask); + +export const useMaskStore = create()( + persist( + (set, get) => ({ + ...DEFAULT_MASK_STATE, + + create(mask) { + set(() => ({ globalMaskId: get().globalMaskId + 1 })); + const id = get().globalMaskId; + const masks = get().masks; + masks[id] = { + ...createEmptyMask(), + ...mask, + id, + }; + + set(() => ({ masks })); + + return masks[id]; + }, + update(id, updater) { + const masks = get().masks; + const mask = masks[id]; + if (!mask) return; + const updateMask = { ...mask }; + updater(updateMask); + masks[id] = updateMask; + set(() => ({ masks })); + }, + delete(id) { + const masks = get().masks; + delete masks[id]; + set(() => ({ masks })); + }, + + get(id) { + return get().masks[id ?? 1145141919810]; + }, + getAll() { + const userMasks = Object.values(get().masks).sort( + (a, b) => b.id - a.id, + ); + return userMasks.concat(BUILTIN_MASKS); + }, + search(text) { + return Object.values(get().masks); + }, + }), + { + name: StoreKey.Mask, + version: 2, + }, + ), +); diff --git a/app/store/prompt.ts b/app/store/prompt.ts index 8d754ff5..e3a2eedd 100644 --- a/app/store/prompt.ts +++ b/app/store/prompt.ts @@ -2,6 +2,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import Fuse from "fuse.js"; import { getLang } from "../locales"; +import { StoreKey } from "../constant"; export interface Prompt { id?: number; @@ -23,8 +24,6 @@ export interface PromptStore { updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void; } -export const PROMPT_KEY = "prompt-store"; - export const SearchService = { ready: false, builtinEngine: new Fuse([], { keys: ["title"] }), @@ -123,7 +122,7 @@ export const usePromptStore = create()( }, }), { - name: PROMPT_KEY, + name: StoreKey.Prompt, version: 1, onRehydrateStorage(state) { const PROMPT_URL = "./prompts.json"; diff --git a/app/store/update.ts b/app/store/update.ts index 47b190b8..888741b8 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant"; +import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant"; import { requestUsage } from "../requests"; export interface UpdateStore { @@ -16,8 +16,6 @@ export interface UpdateStore { updateUsage: (force?: boolean) => Promise; } -export const UPDATE_KEY = "chat-update"; - function queryMeta(key: string, defaultValue?: string): string { let ret: string; if (document) { @@ -84,7 +82,7 @@ export const useUpdateStore = create()( }, }), { - name: UPDATE_KEY, + name: StoreKey.Update, version: 1, }, ), diff --git a/app/styles/globals.scss b/app/styles/globals.scss index 8a7b457a..e7b6de3b 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -1,3 +1,6 @@ +@import "./animation.scss"; +@import "./window.scss"; + @mixin light { --theme: light; @@ -312,3 +315,36 @@ pre { overflow: auto; } } + +.password-input-container { + max-width: 50%; + display: flex; + justify-content: flex-end; + + .password-eye { + margin-right: 4px; + } + + .password-input { + min-width: 80%; + } +} + +.user-avatar { + height: 30px; + min-height: 30px; + width: 30px; + min-width: 30px; + display: flex; + align-items: center; + justify-content: center; + border: var(--border-in-light); + box-shadow: var(--card-shadow); + border-radius: 10px; +} + +.one-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/app/components/window.scss b/app/styles/window.scss similarity index 100% rename from app/components/window.scss rename to app/styles/window.scss diff --git a/app/utils.ts b/app/utils.ts index 1da65c99..95bbed2c 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -1,4 +1,3 @@ -import { EmojiStyle } from "emoji-picker-react"; import { useEffect, useState } from "react"; import { showToast } from "./components/ui-lib"; import Locale from "./locales"; @@ -48,11 +47,18 @@ export function isIOS() { return /iphone|ipad|ipod/.test(userAgent); } -export function useMobileScreen() { - const [isMobileScreen_, setIsMobileScreen] = useState(isMobileScreen()); +export function useWindowSize() { + const [size, setSize] = useState({ + width: window.innerWidth, + height: window.innerHeight, + }); + useEffect(() => { const onResize = () => { - setIsMobileScreen(isMobileScreen()); + setSize({ + width: window.innerWidth, + height: window.innerHeight, + }); }; window.addEventListener("resize", onResize); @@ -62,14 +68,21 @@ export function useMobileScreen() { }; }, []); - return isMobileScreen_; + return size; +} + +export const MOBILE_MAX_WIDTH = 600; +export function useMobileScreen() { + const { width } = useWindowSize(); + + return width <= MOBILE_MAX_WIDTH; } export function isMobileScreen() { if (typeof window === "undefined") { return false; } - return window.innerWidth <= 600; + return window.innerWidth <= MOBILE_MAX_WIDTH; } export function isFirefox() { @@ -90,10 +103,6 @@ export function selectOrCopy(el: HTMLElement, content: string) { return true; } -export function getEmojiUrl(unified: string, style: EmojiStyle) { - return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`; -} - function getDomContentWidth(dom: HTMLElement) { const style = window.getComputedStyle(dom); const paddingWidth = diff --git a/docs/images/cover.png b/docs/images/cover.png index 20fbf6a8..38157305 100644 Binary files a/docs/images/cover.png and b/docs/images/cover.png differ diff --git a/docs/images/more.png b/docs/images/more.png index dfdb3580..70c0d315 100644 Binary files a/docs/images/more.png and b/docs/images/more.png differ diff --git a/docs/images/settings.png b/docs/images/settings.png index e1cdd07e..31603daa 100644 Binary files a/docs/images/settings.png and b/docs/images/settings.png differ