mask.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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 CopyIcon from "../icons/copy.svg";
  10. import { DEFAULT_MASK_AVATAR, DEFAULT_MASK_ID, Mask } from "../store/mask";
  11. import {
  12. Message,
  13. ModelConfig,
  14. ROLES,
  15. useAppConfig,
  16. useChatStore,
  17. } from "../store";
  18. import { Input, List, ListItem, Modal, Popover } from "./ui-lib";
  19. import { Avatar, AvatarPicker, EmojiAvatar } from "./emoji";
  20. import Locale from "../locales";
  21. import { useNavigate } from "react-router-dom";
  22. import chatStyle from "./chat.module.scss";
  23. import { useState } from "react";
  24. import { copyToClipboard } from "../utils";
  25. import { Updater } from "../api/openai/typing";
  26. import { ModelConfigList } from "./model-config";
  27. export function MaskConfig(props: {
  28. mask: Mask;
  29. updateMask: Updater<Mask>;
  30. extraListItems?: JSX.Element;
  31. }) {
  32. const [showPicker, setShowPicker] = useState(false);
  33. const updateConfig = (updater: (config: ModelConfig) => void) => {
  34. const config = { ...props.mask.modelConfig };
  35. updater(config);
  36. props.updateMask((mask) => (mask.modelConfig = config));
  37. };
  38. return (
  39. <>
  40. <ContextPrompts
  41. context={props.mask.context}
  42. updateContext={(updater) => {
  43. const context = props.mask.context.slice();
  44. updater(context);
  45. props.updateMask((mask) => (mask.context = context));
  46. }}
  47. />
  48. <List>
  49. <ListItem title={"角色头像"}>
  50. <Popover
  51. content={
  52. <AvatarPicker
  53. onEmojiClick={(emoji) => {
  54. props.updateMask((mask) => (mask.avatar = emoji));
  55. setShowPicker(false);
  56. }}
  57. ></AvatarPicker>
  58. }
  59. open={showPicker}
  60. onClose={() => setShowPicker(false)}
  61. >
  62. <div
  63. onClick={() => setShowPicker(true)}
  64. style={{ cursor: "pointer" }}
  65. >
  66. {props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
  67. <Avatar avatar={props.mask.avatar} />
  68. ) : (
  69. <Avatar model={props.mask.modelConfig.model} />
  70. )}
  71. </div>
  72. </Popover>
  73. </ListItem>
  74. <ListItem title={"角色名称"}>
  75. <input
  76. type="text"
  77. value={props.mask.name}
  78. onInput={(e) =>
  79. props.updateMask((mask) => (mask.name = e.currentTarget.value))
  80. }
  81. ></input>
  82. </ListItem>
  83. </List>
  84. <List>
  85. <ModelConfigList
  86. modelConfig={{ ...props.mask.modelConfig }}
  87. updateConfig={updateConfig}
  88. />
  89. {props.extraListItems}
  90. </List>
  91. </>
  92. );
  93. }
  94. export function ContextPrompts(props: {
  95. context: Message[];
  96. updateContext: (updater: (context: Message[]) => void) => void;
  97. }) {
  98. const context = props.context;
  99. const addContextPrompt = (prompt: Message) => {
  100. props.updateContext((context) => context.push(prompt));
  101. };
  102. const removeContextPrompt = (i: number) => {
  103. props.updateContext((context) => context.splice(i, 1));
  104. };
  105. const updateContextPrompt = (i: number, prompt: Message) => {
  106. props.updateContext((context) => (context[i] = prompt));
  107. };
  108. return (
  109. <>
  110. <div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
  111. {context.map((c, i) => (
  112. <div className={chatStyle["context-prompt-row"]} key={i}>
  113. <select
  114. value={c.role}
  115. className={chatStyle["context-role"]}
  116. onChange={(e) =>
  117. updateContextPrompt(i, {
  118. ...c,
  119. role: e.target.value as any,
  120. })
  121. }
  122. >
  123. {ROLES.map((r) => (
  124. <option key={r} value={r}>
  125. {r}
  126. </option>
  127. ))}
  128. </select>
  129. <Input
  130. value={c.content}
  131. type="text"
  132. className={chatStyle["context-content"]}
  133. rows={1}
  134. onInput={(e) =>
  135. updateContextPrompt(i, {
  136. ...c,
  137. content: e.currentTarget.value as any,
  138. })
  139. }
  140. />
  141. <IconButton
  142. icon={<DeleteIcon />}
  143. className={chatStyle["context-delete-button"]}
  144. onClick={() => removeContextPrompt(i)}
  145. bordered
  146. />
  147. </div>
  148. ))}
  149. <div className={chatStyle["context-prompt-row"]}>
  150. <IconButton
  151. icon={<AddIcon />}
  152. text={Locale.Context.Add}
  153. bordered
  154. className={chatStyle["context-prompt-button"]}
  155. onClick={() =>
  156. addContextPrompt({
  157. role: "system",
  158. content: "",
  159. date: "",
  160. })
  161. }
  162. />
  163. </div>
  164. </div>
  165. </>
  166. );
  167. }
  168. export function MaskPage() {
  169. const config = useAppConfig();
  170. const navigate = useNavigate();
  171. const masks: Mask[] = new Array(10).fill(0).map((m, i) => ({
  172. id: i,
  173. avatar: "1f606",
  174. name: "预设角色 " + i.toString(),
  175. context: [
  176. { role: "assistant", content: "你好,有什么可以帮忙的吗", date: "" },
  177. ],
  178. modelConfig: config.modelConfig,
  179. lang: "cn",
  180. }));
  181. return (
  182. <ErrorBoundary>
  183. <div className={styles["mask-page"]}>
  184. <div className="window-header">
  185. <div className="window-header-title">
  186. <div className="window-header-main-title">预设角色面具</div>
  187. <div className="window-header-submai-title">编辑预设角色定义</div>
  188. </div>
  189. <div className="window-actions">
  190. <div className="window-action-button">
  191. <IconButton icon={<AddIcon />} bordered />
  192. </div>
  193. <div className="window-action-button">
  194. <IconButton icon={<DownloadIcon />} bordered />
  195. </div>
  196. <div className="window-action-button">
  197. <IconButton
  198. icon={<CloseIcon />}
  199. bordered
  200. onClick={() => navigate(-1)}
  201. />
  202. </div>
  203. </div>
  204. </div>
  205. <div className={styles["mask-page-body"]}>
  206. <input
  207. type="text"
  208. className={styles["search-bar"]}
  209. placeholder="搜索面具"
  210. />
  211. <List>
  212. {masks.map((m) => (
  213. <ListItem
  214. title={m.name}
  215. key={m.id}
  216. subTitle={`包含 ${m.context.length} 条预设对话 / ${
  217. Locale.Settings.Lang.Options[m.lang]
  218. } / ${m.modelConfig.model}`}
  219. icon={
  220. <div className={styles["mask-icon"]}>
  221. <EmojiAvatar avatar={m.avatar} size={20} />
  222. </div>
  223. }
  224. className={styles["mask-item"]}
  225. >
  226. <div className={styles["mask-actions"]}>
  227. <IconButton icon={<AddIcon />} text="对话" />
  228. <IconButton icon={<EditIcon />} text="编辑" />
  229. <IconButton icon={<DeleteIcon />} text="删除" />
  230. </div>
  231. </ListItem>
  232. ))}
  233. </List>
  234. </div>
  235. </div>
  236. </ErrorBoundary>
  237. );
  238. }