plugin.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import { useDebouncedCallback } from "use-debounce";
  2. import OpenAPIClientAxios from "openapi-client-axios";
  3. import yaml from "js-yaml";
  4. import { IconButton } from "./button";
  5. import { ErrorBoundary } from "./error";
  6. import styles from "./mask.module.scss";
  7. import pluginStyles from "./plugin.module.scss";
  8. import DownloadIcon from "../icons/download.svg";
  9. import EditIcon from "../icons/edit.svg";
  10. import AddIcon from "../icons/add.svg";
  11. import CloseIcon from "../icons/close.svg";
  12. import DeleteIcon from "../icons/delete.svg";
  13. import EyeIcon from "../icons/eye.svg";
  14. import CopyIcon from "../icons/copy.svg";
  15. import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
  16. import {
  17. Input,
  18. List,
  19. ListItem,
  20. Modal,
  21. Popover,
  22. Select,
  23. showConfirm,
  24. showToast,
  25. } from "./ui-lib";
  26. import { downloadAs } from "../utils";
  27. import Locale from "../locales";
  28. import { useNavigate } from "react-router-dom";
  29. import { useEffect, useState } from "react";
  30. import { Path } from "../constant";
  31. import { nanoid } from "nanoid";
  32. export function PluginPage() {
  33. const navigate = useNavigate();
  34. const pluginStore = usePluginStore();
  35. const allPlugins = pluginStore.getAll();
  36. const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
  37. const [searchText, setSearchText] = useState("");
  38. const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
  39. // refactored already, now it accurate
  40. const onSearch = (text: string) => {
  41. setSearchText(text);
  42. if (text.length > 0) {
  43. const result = allPlugins.filter(
  44. (m) => m?.title.toLowerCase().includes(text.toLowerCase()),
  45. );
  46. setSearchPlugins(result);
  47. } else {
  48. setSearchPlugins(allPlugins);
  49. }
  50. };
  51. const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
  52. const editingPlugin = pluginStore.get(editingPluginId);
  53. const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
  54. const closePluginModal = () => setEditingPluginId(undefined);
  55. const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
  56. const content = e.target.innerText;
  57. try {
  58. const api = new OpenAPIClientAxios({
  59. definition: yaml.load(content) as any,
  60. });
  61. api
  62. .init()
  63. .then(() => {
  64. if (content != editingPlugin.content) {
  65. pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
  66. plugin.content = content;
  67. const tool = FunctionToolService.add(plugin, true);
  68. plugin.title = tool.api.definition.info.title;
  69. plugin.version = tool.api.definition.info.version;
  70. });
  71. }
  72. })
  73. .catch((e) => {
  74. console.error(e);
  75. showToast(Locale.Plugin.EditModal.Error);
  76. });
  77. } catch (e) {
  78. console.error(e);
  79. showToast(Locale.Plugin.EditModal.Error);
  80. }
  81. }, 100).bind(null, editingPlugin);
  82. return (
  83. <ErrorBoundary>
  84. <div className={styles["mask-page"]}>
  85. <div className="window-header">
  86. <div className="window-header-title">
  87. <div className="window-header-main-title">
  88. {Locale.Plugin.Page.Title}
  89. </div>
  90. <div className="window-header-submai-title">
  91. {Locale.Plugin.Page.SubTitle(plugins.length)}
  92. </div>
  93. </div>
  94. <div className="window-actions">
  95. <div className="window-action-button">
  96. <IconButton
  97. icon={<CloseIcon />}
  98. bordered
  99. onClick={() => navigate(-1)}
  100. />
  101. </div>
  102. </div>
  103. </div>
  104. <div className={styles["mask-page-body"]}>
  105. <div className={styles["mask-filter"]}>
  106. <input
  107. type="text"
  108. className={styles["search-bar"]}
  109. placeholder={Locale.Plugin.Page.Search}
  110. autoFocus
  111. onInput={(e) => onSearch(e.currentTarget.value)}
  112. />
  113. <IconButton
  114. className={styles["mask-create"]}
  115. icon={<AddIcon />}
  116. text={Locale.Plugin.Page.Create}
  117. bordered
  118. onClick={() => {
  119. const createdPlugin = pluginStore.create();
  120. setEditingPluginId(createdPlugin.id);
  121. }}
  122. />
  123. </div>
  124. <div>
  125. {plugins.map((m) => (
  126. <div className={styles["mask-item"]} key={m.id}>
  127. <div className={styles["mask-header"]}>
  128. <div className={styles["mask-icon"]}></div>
  129. <div className={styles["mask-title"]}>
  130. <div className={styles["mask-name"]}>
  131. {m.title}@<small>{m.version}</small>
  132. </div>
  133. <div className={styles["mask-info"] + " one-line"}>
  134. {Locale.Plugin.Item.Info(
  135. FunctionToolService.add(m).length,
  136. )}
  137. </div>
  138. </div>
  139. </div>
  140. <div className={styles["mask-actions"]}>
  141. {m.builtin ? (
  142. <IconButton
  143. icon={<EyeIcon />}
  144. text={Locale.Plugin.Item.View}
  145. onClick={() => setEditingPluginId(m.id)}
  146. />
  147. ) : (
  148. <IconButton
  149. icon={<EditIcon />}
  150. text={Locale.Plugin.Item.Edit}
  151. onClick={() => setEditingPluginId(m.id)}
  152. />
  153. )}
  154. {!m.builtin && (
  155. <IconButton
  156. icon={<DeleteIcon />}
  157. text={Locale.Plugin.Item.Delete}
  158. onClick={async () => {
  159. if (
  160. await showConfirm(Locale.Plugin.Item.DeleteConfirm)
  161. ) {
  162. pluginStore.delete(m.id);
  163. }
  164. }}
  165. />
  166. )}
  167. </div>
  168. </div>
  169. ))}
  170. </div>
  171. </div>
  172. </div>
  173. {editingPlugin && (
  174. <div className="modal-mask">
  175. <Modal
  176. title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)}
  177. onClose={closePluginModal}
  178. actions={[
  179. <IconButton
  180. icon={<DownloadIcon />}
  181. text={Locale.Plugin.EditModal.Download}
  182. key="export"
  183. bordered
  184. onClick={() =>
  185. downloadAs(
  186. JSON.stringify(editingPlugin),
  187. `${editingPlugin.title}@${editingPlugin.version}.json`,
  188. )
  189. }
  190. />,
  191. ]}
  192. >
  193. <div className={styles["mask-page"]}>
  194. <div className={pluginStyles["plugin-title"]}>
  195. {Locale.Plugin.EditModal.Content}
  196. </div>
  197. <div
  198. className={`markdown-body ${pluginStyles["plugin-content"]}`}
  199. dir="auto"
  200. >
  201. <pre>
  202. <code
  203. contentEditable={true}
  204. dangerouslySetInnerHTML={{ __html: editingPlugin.content }}
  205. onBlur={onChangePlugin}
  206. ></code>
  207. </pre>
  208. </div>
  209. <div className={pluginStyles["plugin-title"]}>
  210. {Locale.Plugin.EditModal.Method}
  211. </div>
  212. <div className={styles["mask-page-body"]} style={{ padding: 0 }}>
  213. {editingPluginTool?.tools.map((tool, index) => (
  214. <div className={styles["mask-item"]} key={index}>
  215. <div className={styles["mask-header"]}>
  216. <div className={styles["mask-title"]}>
  217. <div className={styles["mask-name"]}>
  218. {tool?.function?.name}
  219. </div>
  220. <div className={styles["mask-info"] + " one-line"}>
  221. {tool?.function?.description}
  222. </div>
  223. </div>
  224. </div>
  225. </div>
  226. ))}
  227. </div>
  228. </div>
  229. </Modal>
  230. </div>
  231. )}
  232. </ErrorBoundary>
  233. );
  234. }