feat: add i18n for mask

This commit is contained in:
Yidadaa 2023-04-27 01:16:21 +08:00
parent 3cda44e05b
commit c7c58ef031
22 changed files with 890 additions and 81 deletions

View File

@ -13,6 +13,7 @@ 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";
@ -58,7 +59,7 @@ import { Path } from "../constant";
import { ModelConfigList } from "./model-config";
import { Avatar, AvatarPicker } from "./emoji";
import { MaskConfig } from "./mask";
import { DEFAULT_MASK_ID } from "../store/mask";
import { DEFAULT_MASK_ID, useMaskStore } from "../store/mask";
const Markdown = dynamic(
async () => memo((await import("./markdown")).Markdown),
@ -108,6 +109,8 @@ 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 (
<div className="modal-mask">
@ -117,9 +120,9 @@ export function SessionConfigModel(props: { onClose: () => void }) {
actions={[
<IconButton
key="reset"
icon={<CopyIcon />}
icon={<ResetIcon />}
bordered
text="重置"
text={Locale.Chat.Config.Reset}
onClick={() =>
confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession()
}
@ -128,8 +131,13 @@ export function SessionConfigModel(props: { onClose: () => void }) {
key="copy"
icon={<CopyIcon />}
bordered
text="保存为面具"
onClick={() => copyToClipboard(session.memoryPrompt)}
text={Locale.Chat.Config.SaveAs}
onClick={() => {
navigate(Path.Masks);
setTimeout(() => {
maskStore.create(session.mask);
}, 500);
}}
/>,
]}
>

View File

@ -20,11 +20,36 @@
padding: 20px;
overflow-y: auto;
.search-bar {
.mask-filter {
width: 100%;
max-width: 100%;
margin-bottom: 20px;
margin-bottom: 10px;
animation: search-in ease 0.3s;
display: flex;
.search-bar {
flex-grow: 1;
max-width: 100%;
min-width: 0;
margin-bottom: 20px;
animation: search-in ease 0.3s;
}
.mask-filter-lang {
height: 100%;
margin-left: 10px;
}
.mask-create {
height: 100%;
margin-left: 10px;
box-sizing: border-box;
button {
padding: 10px;
}
}
}
.mask-item {

View File

@ -4,37 +4,28 @@ import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
import DownloadIcon from "../icons/download.svg";
import UploadIcon from "../icons/upload.svg";
import EditIcon from "../icons/edit.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import BotIcon from "../icons/bot.svg";
import EyeIcon from "../icons/eye.svg";
import CopyIcon from "../icons/copy.svg";
import {
DEFAULT_MASK_AVATAR,
DEFAULT_MASK_ID,
Mask,
useMaskStore,
} from "../store/mask";
import {
Message,
ModelConfig,
ROLES,
useAppConfig,
useChatStore,
} from "../store";
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
import { Message, ModelConfig, ROLES, useChatStore } from "../store";
import { Input, List, ListItem, Modal, Popover, showToast } from "./ui-lib";
import { Avatar, AvatarPicker, EmojiAvatar } from "./emoji";
import Locale from "../locales";
import { Avatar, AvatarPicker } from "./emoji";
import Locale, { AllLangs, Lang } from "../locales";
import { useNavigate } from "react-router-dom";
import chatStyle from "./chat.module.scss";
import { useState } from "react";
import { copyToClipboard } from "../utils";
import { useEffect, useState } from "react";
import { downloadAs } from "../utils";
import { Updater } from "../api/openai/typing";
import { ModelConfigList } from "./model-config";
import { Path } from "../constant";
import { FileName, Path } from "../constant";
import { BUILTIN_MASK_STORE } from "../masks";
export function MaskAvatar(props: { mask: Mask }) {
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
@ -48,10 +39,13 @@ export function MaskConfig(props: {
mask: Mask;
updateMask: Updater<Mask>;
extraListItems?: JSX.Element;
readonly?: boolean;
}) {
const [showPicker, setShowPicker] = useState(false);
const updateConfig = (updater: (config: ModelConfig) => void) => {
if (props.readonly) return;
const config = { ...props.mask.modelConfig };
updater(config);
props.updateMask((mask) => (mask.modelConfig = config));
@ -69,7 +63,7 @@ export function MaskConfig(props: {
/>
<List>
<ListItem title={"角色头像"}>
<ListItem title={Locale.Mask.Config.Avatar}>
<Popover
content={
<AvatarPicker
@ -90,7 +84,7 @@ export function MaskConfig(props: {
</div>
</Popover>
</ListItem>
<ListItem title={"角色名称"}>
<ListItem title={Locale.Mask.Config.Name}>
<input
type="text"
value={props.mask.name}
@ -197,33 +191,64 @@ export function MaskPage() {
const maskStore = useMaskStore();
const chatStore = useChatStore();
const masks = maskStore.getAll();
const [filterLang, setFilterLang] = useState<Lang>();
const allMasks = maskStore
.getAll()
.filter((m) => !filterLang || m.lang === filterLang);
const [searchMasks, setSearchMasks] = useState<Mask[]>([]);
const [searchText, setSearchText] = useState("");
const masks = searchText.length > 0 ? searchMasks : allMasks;
// simple search, will refactor later
const onSearch = (text: string) => {
setSearchText(text);
if (text.length > 0) {
const result = allMasks.filter((m) => m.name.includes(text));
setSearchMasks(result);
} else {
setSearchMasks(allMasks);
}
};
const [editingMaskId, setEditingMaskId] = useState<number | undefined>();
const editingMask = maskStore.get(editingMaskId);
const editingMask =
maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
const closeMaskModal = () => setEditingMaskId(undefined);
const downloadAll = () => {
downloadAs(JSON.stringify(masks), FileName.Masks);
};
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title"></div>
<div className="window-header-main-title">
{Locale.Mask.Page.Title}
</div>
<div className="window-header-submai-title">
{masks.length}
{Locale.Mask.Page.SubTitle(allMasks.length)}
</div>
</div>
<div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<AddIcon />}
icon={<DownloadIcon />}
bordered
onClick={() => maskStore.create()}
onClick={downloadAll}
/>
</div>
<div className="window-action-button">
<IconButton icon={<DownloadIcon />} bordered />
<IconButton
icon={<UploadIcon />}
bordered
onClick={() => showToast(Locale.WIP)}
/>
</div>
<div className="window-action-button">
<IconButton
@ -236,12 +261,45 @@ export function MaskPage() {
</div>
<div className={styles["mask-page-body"]}>
<input
type="text"
className={styles["search-bar"]}
placeholder="搜索"
autoFocus
/>
<div className={styles["mask-filter"]}>
<input
type="text"
className={styles["search-bar"]}
placeholder={Locale.Mask.Page.Search}
autoFocus
onInput={(e) => onSearch(e.currentTarget.value)}
/>
<select
className={styles["mask-filter-lang"]}
value={filterLang ?? Locale.Settings.Lang.All}
onChange={(e) => {
const value = e.currentTarget.value;
if (value === Locale.Settings.Lang.All) {
setFilterLang(undefined);
} else {
setFilterLang(value as Lang);
}
}}
>
<option key="all" value={Locale.Settings.Lang.All}>
{Locale.Settings.Lang.All}
</option>
{AllLangs.map((lang) => (
<option value={lang} key={lang}>
{Locale.Settings.Lang.Options[lang]}
</option>
))}
</select>
<div className={styles["mask-create"]}>
<IconButton
icon={<AddIcon />}
text={Locale.Mask.Page.Create}
bordered
onClick={() => maskStore.create()}
/>
</div>
</div>
<div>
{masks.map((m) => (
@ -253,7 +311,7 @@ export function MaskPage() {
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{m.name}</div>
<div className={styles["mask-info"] + " one-line"}>
{`包含 ${m.context.length} 条预设对话 / ${
{`${Locale.Mask.Item.Info(m.context.length)} / ${
Locale.Settings.Lang.Options[m.lang]
} / ${m.modelConfig.model}`}
</div>
@ -262,26 +320,36 @@ export function MaskPage() {
<div className={styles["mask-actions"]}>
<IconButton
icon={<AddIcon />}
text="对话"
text={Locale.Mask.Item.Chat}
onClick={() => {
chatStore.newSession(m);
navigate(Path.Chat);
}}
/>
<IconButton
icon={<EditIcon />}
text="编辑"
onClick={() => setEditingMaskId(m.id)}
/>
<IconButton
icon={<DeleteIcon />}
text="删除"
onClick={() => {
if (confirm("确认删除?")) {
maskStore.delete(m.id);
}
}}
/>
{m.builtin ? (
<IconButton
icon={<EyeIcon />}
text={Locale.Mask.Item.View}
onClick={() => setEditingMaskId(m.id)}
/>
) : (
<IconButton
icon={<EditIcon />}
text={Locale.Mask.Item.Edit}
onClick={() => setEditingMaskId(m.id)}
/>
)}
{!m.builtin && (
<IconButton
icon={<DeleteIcon />}
text={Locale.Mask.Item.Delete}
onClick={() => {
if (confirm(Locale.Mask.Item.DeleteConfirm)) {
maskStore.delete(m.id);
}
}}
/>
)}
</div>
</div>
))}
@ -291,12 +359,35 @@ export function MaskPage() {
{editingMask && (
<div className="modal-mask">
<Modal title="编辑预设面具" onClose={closeMaskModal}>
<Modal
title={Locale.Mask.EditModal.Title(editingMask?.builtin)}
onClose={closeMaskModal}
actions={[
<IconButton
icon={<DownloadIcon />}
text={Locale.Mask.EditModal.Download}
key="export"
bordered
/>,
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.Mask.EditModal.Clone}
onClick={() => {
navigate(Path.Masks);
maskStore.create(editingMask);
setEditingMaskId(undefined);
}}
/>,
]}
>
<MaskConfig
mask={editingMask!}
mask={editingMask}
updateMask={(updater) =>
maskStore.update(editingMaskId!, updater)
}
readonly={editingMask.builtin}
/>
</Modal>
</div>

View File

@ -6,7 +6,7 @@ import styles from "./new-chat.module.scss";
import LeftIcon from "../icons/left.svg";
import { useNavigate } from "react-router-dom";
import { createEmptyMask, Mask, useMaskStore } from "../store/mask";
import { useWindowSize } from "../utils";
import Locale from "../locales";
import { useChatStore } from "../store";
import { MaskAvatar } from "./mask";
@ -59,7 +59,7 @@ function useMaskGroup(masks: Mask[]) {
useEffect(() => {
const appBody = document.getElementById(SlotID.AppBody);
if (!appBody) return;
if (!appBody || masks.length === 0) return;
const rect = appBody.getBoundingClientRect();
const maxWidth = rect.width;
@ -108,10 +108,13 @@ export function NewChat() {
<div className={styles["mask-header"]}>
<IconButton
icon={<LeftIcon />}
text="返回"
onClick={() => navigate(-1)}
text={Locale.NewChat.Return}
onClick={() => navigate(Path.Home)}
></IconButton>
<IconButton
text={Locale.NewChat.Skip}
onClick={() => startChat()}
></IconButton>
<IconButton text="跳过" onClick={() => startChat()}></IconButton>
</div>
<div className={styles["mask-cards"]}>
<div className={styles["mask-card"]}>
@ -125,14 +128,12 @@ export function NewChat() {
</div>
</div>
<div className={styles["title"]}></div>
<div className={styles["sub-title"]}>
</div>
<div className={styles["title"]}>{Locale.NewChat.Title}</div>
<div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
<input
className={styles["search-bar"]}
placeholder="搜索"
placeholder={Locale.NewChat.More}
type="text"
onClick={() => navigate(Path.Masks)}
/>
@ -141,7 +142,11 @@ export function NewChat() {
{groups.map((masks, i) => (
<div key={i} className={styles["mask-row"]}>
{masks.map((mask, index) => (
<MaskItem key={index} mask={mask} onClick={startChat} />
<MaskItem
key={index}
mask={mask}
onClick={() => startChat(mask)}
/>
))}
</div>
))}

View File

@ -143,7 +143,7 @@
.toast-container {
position: fixed;
bottom: 0;
bottom: 10vh;
left: 0;
width: 100vw;
display: flex;

View File

@ -1,3 +0,0 @@
import { Mask } from "../store/mask";
export const BUILTIN_MASKS: Mask[] = [];

View File

@ -19,6 +19,11 @@ export enum SlotID {
AppBody = "app-body",
}
export enum FileName {
Masks = "masks.json",
Prompts = "prompts.json",
}
export const MAX_SIDEBAR_WIDTH = 500;
export const MIN_SIDEBAR_WIDTH = 230;
export const NARROW_SIDEBAR_WIDTH = 100;

1
app/icons/upload.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -30,6 +30,10 @@ const cn = {
return inputHints + "/ 触发补全";
},
Send: "发送",
Config: {
Reset: "重置默认",
SaveAs: "另存为面具",
},
},
Export: {
Title: "导出聊天记录为 Markdown",
@ -68,6 +72,7 @@ const cn = {
},
Lang: {
Name: "Language",
All: "所有语言",
Options: {
cn: "简体中文",
en: "English",
@ -176,6 +181,39 @@ const cn = {
Edit: "当前对话设置",
Add: "新增预设对话",
},
Mask: {
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: "搜索更多",
},
};
export type LocaleType = typeof cn;

View File

@ -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",
@ -70,6 +74,7 @@ const de: LocaleType = {
},
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",
@ -184,6 +189,39 @@ const de: LocaleType = {
Edit: "Kontext- und Gedächtnis-Prompts",
Add: "Hinzufügen",
},
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",
},
};
export default de;

View File

@ -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",
@ -70,6 +74,7 @@ const en: LocaleType = {
},
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",
@ -178,7 +183,40 @@ const en: LocaleType = {
Context: {
Toast: (x: any) => `With ${x} contextual prompts`,
Edit: "Contextual and Memory Prompts",
Add: "Add One",
Add: "Add a Prompt",
},
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",
},
};

View File

@ -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",
@ -70,6 +74,7 @@ const es: LocaleType = {
},
Lang: {
Name: "Language",
All: "All Languages",
Options: {
cn: "简体中文",
en: "Inglés",
@ -182,6 +187,39 @@ const es: LocaleType = {
Edit: "Contextual and Memory Prompts",
Add: "Add One",
},
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",
},
};
export default es;

View File

@ -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",
@ -70,6 +74,7 @@ const it: LocaleType = {
},
Lang: {
Name: "Lingue",
All: "All Languages",
Options: {
cn: "简体中文",
en: "English",
@ -183,6 +188,39 @@ const it: LocaleType = {
Edit: "Prompt contestuali e di memoria",
Add: "Aggiungi altro",
},
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",
},
};
export default it;

View File

@ -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形式でエクスポート",
@ -69,6 +74,7 @@ const jp = {
},
Lang: {
Name: "Language",
All: "所有语言",
Options: {
cn: "简体中文",
en: "English",
@ -182,6 +188,39 @@ const jp = {
Edit: "前置コンテキストと履歴メモリ",
Add: "新規追加",
},
Mask: {
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: "搜索更多",
},
};
export default jp;

View File

@ -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",
@ -70,6 +74,7 @@ const tr: LocaleType = {
},
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",
@ -183,6 +188,39 @@ const tr: LocaleType = {
Edit: "Bağlamsal ve Bellek Komutları",
Add: "Yeni Ekle",
},
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",
},
};
export default tr;

View File

@ -30,6 +30,10 @@ const tw: LocaleType = {
return inputHints;
},
Send: "發送",
Config: {
Reset: "重置默认",
SaveAs: "另存为面具",
},
},
Export: {
Title: "匯出聊天記錄為 Markdown",
@ -68,6 +72,7 @@ const tw: LocaleType = {
},
Lang: {
Name: "Language",
All: "所有语言",
Options: {
cn: "简体中文",
en: "English",
@ -175,6 +180,39 @@ const tw: LocaleType = {
Edit: "前置上下文和歷史記憶",
Add: "新增壹條",
},
Mask: {
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: "搜索更多",
},
};
export default tw;

View File

@ -1,3 +0,0 @@
import { Mask } from "./store/mask";
export const BUILT_IN_MASKS: Mask[] = [];

296
app/masks/cn.ts Normal file
View File

@ -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,
},
];

44
app/masks/en.ts Normal file
View File

@ -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 promptI 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,
},
];

26
app/masks/index.ts Normal file
View File

@ -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<number, Mask>,
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),
);

3
app/masks/typing.ts Normal file
View File

@ -0,0 +1,3 @@
import { type Mask } from "../store/mask";
export type BuiltinMask = Omit<Mask, "id">;

View File

@ -1,5 +1,6 @@
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";
@ -13,6 +14,7 @@ export type Mask = {
context: Message[];
modelConfig: ModelConfig;
lang: Lang;
builtin: boolean;
};
export const DEFAULT_MASK_STATE = {
@ -40,6 +42,7 @@ export const createEmptyMask = () =>
context: [],
modelConfig: { ...useAppConfig.getState().modelConfig },
lang: getLang(),
builtin: false,
} as Mask);
export const useMaskStore = create<MaskStore>()(
@ -53,8 +56,8 @@ export const useMaskStore = create<MaskStore>()(
const masks = get().masks;
masks[id] = {
...createEmptyMask(),
id,
...mask,
id,
};
set(() => ({ masks }));
@ -80,7 +83,10 @@ export const useMaskStore = create<MaskStore>()(
return get().masks[id ?? 1145141919810];
},
getAll() {
return Object.values(get().masks).sort((a, b) => a.id - b.id);
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);