mask.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import { IconButton } from "./button";
  2. import { ErrorBoundary } from "./error";
  3. import styles from "./mask.module.scss";
  4. import DownloadIcon from "../icons/download.svg";
  5. import EditIcon from "../icons/edit.svg";
  6. import AddIcon from "../icons/add.svg";
  7. import CloseIcon from "../icons/close.svg";
  8. import DeleteIcon from "../icons/delete.svg";
  9. import BotIcon from "../icons/bot.svg";
  10. import CopyIcon from "../icons/copy.svg";
  11. import {
  12. DEFAULT_MASK_AVATAR,
  13. DEFAULT_MASK_ID,
  14. Mask,
  15. useMaskStore,
  16. } from "../store/mask";
  17. import {
  18. Message,
  19. ModelConfig,
  20. ROLES,
  21. useAppConfig,
  22. useChatStore,
  23. } from "../store";
  24. import { Input, List, ListItem, Modal, Popover, showToast } from "./ui-lib";
  25. import { Avatar, AvatarPicker, EmojiAvatar } from "./emoji";
  26. import Locale from "../locales";
  27. import { useNavigate } from "react-router-dom";
  28. import chatStyle from "./chat.module.scss";
  29. import { useState } from "react";
  30. import { copyToClipboard } from "../utils";
  31. import { Updater } from "../api/openai/typing";
  32. import { ModelConfigList } from "./model-config";
  33. import { Path } from "../constant";
  34. export function MaskAvatar(props: { mask: Mask }) {
  35. return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
  36. <Avatar avatar={props.mask.avatar} />
  37. ) : (
  38. <Avatar model={props.mask.modelConfig.model} />
  39. );
  40. }
  41. export function MaskConfig(props: {
  42. mask: Mask;
  43. updateMask: Updater<Mask>;
  44. extraListItems?: JSX.Element;
  45. }) {
  46. const [showPicker, setShowPicker] = useState(false);
  47. const updateConfig = (updater: (config: ModelConfig) => void) => {
  48. const config = { ...props.mask.modelConfig };
  49. updater(config);
  50. props.updateMask((mask) => (mask.modelConfig = config));
  51. };
  52. return (
  53. <>
  54. <ContextPrompts
  55. context={props.mask.context}
  56. updateContext={(updater) => {
  57. const context = props.mask.context.slice();
  58. updater(context);
  59. props.updateMask((mask) => (mask.context = context));
  60. }}
  61. />
  62. <List>
  63. <ListItem title={"角色头像"}>
  64. <Popover
  65. content={
  66. <AvatarPicker
  67. onEmojiClick={(emoji) => {
  68. props.updateMask((mask) => (mask.avatar = emoji));
  69. setShowPicker(false);
  70. }}
  71. ></AvatarPicker>
  72. }
  73. open={showPicker}
  74. onClose={() => setShowPicker(false)}
  75. >
  76. <div
  77. onClick={() => setShowPicker(true)}
  78. style={{ cursor: "pointer" }}
  79. >
  80. <MaskAvatar mask={props.mask} />
  81. </div>
  82. </Popover>
  83. </ListItem>
  84. <ListItem title={"角色名称"}>
  85. <input
  86. type="text"
  87. value={props.mask.name}
  88. onInput={(e) =>
  89. props.updateMask((mask) => (mask.name = e.currentTarget.value))
  90. }
  91. ></input>
  92. </ListItem>
  93. </List>
  94. <List>
  95. <ModelConfigList
  96. modelConfig={{ ...props.mask.modelConfig }}
  97. updateConfig={updateConfig}
  98. />
  99. {props.extraListItems}
  100. </List>
  101. </>
  102. );
  103. }
  104. export function ContextPrompts(props: {
  105. context: Message[];
  106. updateContext: (updater: (context: Message[]) => void) => void;
  107. }) {
  108. const context = props.context;
  109. const addContextPrompt = (prompt: Message) => {
  110. props.updateContext((context) => context.push(prompt));
  111. };
  112. const removeContextPrompt = (i: number) => {
  113. props.updateContext((context) => context.splice(i, 1));
  114. };
  115. const updateContextPrompt = (i: number, prompt: Message) => {
  116. props.updateContext((context) => (context[i] = prompt));
  117. };
  118. return (
  119. <>
  120. <div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
  121. {context.map((c, i) => (
  122. <div className={chatStyle["context-prompt-row"]} key={i}>
  123. <select
  124. value={c.role}
  125. className={chatStyle["context-role"]}
  126. onChange={(e) =>
  127. updateContextPrompt(i, {
  128. ...c,
  129. role: e.target.value as any,
  130. })
  131. }
  132. >
  133. {ROLES.map((r) => (
  134. <option key={r} value={r}>
  135. {r}
  136. </option>
  137. ))}
  138. </select>
  139. <Input
  140. value={c.content}
  141. type="text"
  142. className={chatStyle["context-content"]}
  143. rows={1}
  144. onInput={(e) =>
  145. updateContextPrompt(i, {
  146. ...c,
  147. content: e.currentTarget.value as any,
  148. })
  149. }
  150. />
  151. <IconButton
  152. icon={<DeleteIcon />}
  153. className={chatStyle["context-delete-button"]}
  154. onClick={() => removeContextPrompt(i)}
  155. bordered
  156. />
  157. </div>
  158. ))}
  159. <div className={chatStyle["context-prompt-row"]}>
  160. <IconButton
  161. icon={<AddIcon />}
  162. text={Locale.Context.Add}
  163. bordered
  164. className={chatStyle["context-prompt-button"]}
  165. onClick={() =>
  166. addContextPrompt({
  167. role: "system",
  168. content: "",
  169. date: "",
  170. })
  171. }
  172. />
  173. </div>
  174. </div>
  175. </>
  176. );
  177. }
  178. export function MaskPage() {
  179. const navigate = useNavigate();
  180. const maskStore = useMaskStore();
  181. const chatStore = useChatStore();
  182. const masks = maskStore.getAll();
  183. const [editingMaskId, setEditingMaskId] = useState<number | undefined>();
  184. const editingMask = maskStore.get(editingMaskId);
  185. const closeMaskModal = () => setEditingMaskId(undefined);
  186. return (
  187. <ErrorBoundary>
  188. <div className={styles["mask-page"]}>
  189. <div className="window-header">
  190. <div className="window-header-title">
  191. <div className="window-header-main-title">预设角色面具</div>
  192. <div className="window-header-submai-title">
  193. 共有{masks.length} 个预设角色定义
  194. </div>
  195. </div>
  196. <div className="window-actions">
  197. <div className="window-action-button">
  198. <IconButton
  199. icon={<AddIcon />}
  200. bordered
  201. onClick={() => maskStore.create()}
  202. />
  203. </div>
  204. <div className="window-action-button">
  205. <IconButton icon={<DownloadIcon />} bordered />
  206. </div>
  207. <div className="window-action-button">
  208. <IconButton
  209. icon={<CloseIcon />}
  210. bordered
  211. onClick={() => navigate(-1)}
  212. />
  213. </div>
  214. </div>
  215. </div>
  216. <div className={styles["mask-page-body"]}>
  217. <input
  218. type="text"
  219. className={styles["search-bar"]}
  220. placeholder="搜索"
  221. autoFocus
  222. />
  223. <div>
  224. {masks.map((m) => (
  225. <div className={styles["mask-item"]} key={m.id}>
  226. <div className={styles["mask-header"]}>
  227. <div className={styles["mask-icon"]}>
  228. <MaskAvatar mask={m} />
  229. </div>
  230. <div className={styles["mask-title"]}>
  231. <div className={styles["mask-name"]}>{m.name}</div>
  232. <div className={styles["mask-info"] + " one-line"}>
  233. {`包含 ${m.context.length} 条预设对话 / ${
  234. Locale.Settings.Lang.Options[m.lang]
  235. } / ${m.modelConfig.model}`}
  236. </div>
  237. </div>
  238. </div>
  239. <div className={styles["mask-actions"]}>
  240. <IconButton
  241. icon={<AddIcon />}
  242. text="对话"
  243. onClick={() => {
  244. chatStore.newSession(m);
  245. navigate(Path.Chat);
  246. }}
  247. />
  248. <IconButton
  249. icon={<EditIcon />}
  250. text="编辑"
  251. onClick={() => setEditingMaskId(m.id)}
  252. />
  253. <IconButton
  254. icon={<DeleteIcon />}
  255. text="删除"
  256. onClick={() => {
  257. if (confirm("确认删除?")) {
  258. maskStore.delete(m.id);
  259. }
  260. }}
  261. />
  262. </div>
  263. </div>
  264. ))}
  265. </div>
  266. </div>
  267. </div>
  268. {editingMask && (
  269. <div className="modal-mask">
  270. <Modal title="编辑预设面具" onClose={closeMaskModal}>
  271. <MaskConfig
  272. mask={editingMask!}
  273. updateMask={(updater) =>
  274. maskStore.update(editingMaskId!, updater)
  275. }
  276. />
  277. </Modal>
  278. </div>
  279. )}
  280. </ErrorBoundary>
  281. );
  282. }