Spaces:
Build error
Build error
| import { IconButton } from "./button"; | |
| import { ErrorBoundary } from "./error"; | |
| import styles from "./plugin.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 EyeIcon from "../icons/eye.svg"; | |
| import CopyIcon from "../icons/copy.svg"; | |
| import LeftIcon from "../icons/left.svg"; | |
| import { Plugin, usePluginStore } from "../store/plugin"; | |
| import { | |
| ChatMessage, | |
| createMessage, | |
| ModelConfig, | |
| useAppConfig, | |
| useChatStore, | |
| } from "../store"; | |
| import { ROLES } from "../client/api"; | |
| import { | |
| Input, | |
| List, | |
| ListItem, | |
| Modal, | |
| Popover, | |
| Select, | |
| showConfirm, | |
| } from "./ui-lib"; | |
| import { Avatar, AvatarPicker } from "./emoji"; | |
| import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales"; | |
| import { useLocation, useNavigate } from "react-router-dom"; | |
| import chatStyle from "./chat.module.scss"; | |
| import { useEffect, useState } from "react"; | |
| import { copyToClipboard, downloadAs, readFromFile } from "../utils"; | |
| import { Updater } from "../typing"; | |
| import { ModelConfigList } from "./model-config"; | |
| import { FileName, Path } from "../constant"; | |
| import { BUILTIN_PLUGIN_STORE } from "../plugins"; | |
| import { nanoid } from "nanoid"; | |
| import { getISOLang, getLang } from "../locales"; | |
| // export function PluginConfig(props: { | |
| // plugin: Plugin; | |
| // updateMask: Updater<Plugin>; | |
| // extraListItems?: JSX.Element; | |
| // readonly?: boolean; | |
| // shouldSyncFromGlobal?: 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; | |
| // // // if user changed current session mask, it will disable auto sync | |
| // // mask.syncGlobalConfig = false; | |
| // }); | |
| // }; | |
| // const globalConfig = useAppConfig(); | |
| // return ( | |
| // <> | |
| // <ContextPrompts | |
| // context={props.mask.context} | |
| // updateContext={(updater) => { | |
| // const context = props.mask.context.slice(); | |
| // updater(context); | |
| // props.updateMask((mask) => (mask.context = context)); | |
| // }} | |
| // /> | |
| // <List> | |
| // <ListItem title={Locale.Mask.Config.Avatar}> | |
| // <Popover | |
| // content={ | |
| // <AvatarPicker | |
| // onEmojiClick={(emoji) => { | |
| // props.updateMask((mask) => (mask.avatar = emoji)); | |
| // setShowPicker(false); | |
| // }} | |
| // ></AvatarPicker> | |
| // } | |
| // open={showPicker} | |
| // onClose={() => setShowPicker(false)} | |
| // > | |
| // <div | |
| // onClick={() => setShowPicker(true)} | |
| // style={{ cursor: "pointer" }} | |
| // > | |
| // </div> | |
| // </Popover> | |
| // </ListItem> | |
| // <ListItem title={Locale.Mask.Config.Name}> | |
| // <input | |
| // type="text" | |
| // value={props.mask.name} | |
| // onInput={(e) => | |
| // props.updateMask((mask) => { | |
| // mask.name = e.currentTarget.value; | |
| // }) | |
| // } | |
| // ></input> | |
| // </ListItem> | |
| // <ListItem | |
| // title={Locale.Mask.Config.HideContext.Title} | |
| // subTitle={Locale.Mask.Config.HideContext.SubTitle} | |
| // > | |
| // <input | |
| // type="checkbox" | |
| // checked={props.mask.hideContext} | |
| // onChange={(e) => { | |
| // props.updateMask((mask) => { | |
| // mask.hideContext = e.currentTarget.checked; | |
| // }); | |
| // }} | |
| // ></input> | |
| // </ListItem> | |
| // {!props.shouldSyncFromGlobal ? ( | |
| // <ListItem | |
| // title={Locale.Mask.Config.Share.Title} | |
| // subTitle={Locale.Mask.Config.Share.SubTitle} | |
| // > | |
| // <IconButton | |
| // icon={<CopyIcon />} | |
| // text={Locale.Mask.Config.Share.Action} | |
| // onClick={copyMaskLink} | |
| // /> | |
| // </ListItem> | |
| // ) : null} | |
| // {props.shouldSyncFromGlobal ? ( | |
| // <ListItem | |
| // title={Locale.Mask.Config.Sync.Title} | |
| // subTitle={Locale.Mask.Config.Sync.SubTitle} | |
| // > | |
| // <input | |
| // type="checkbox" | |
| // checked={props.mask.syncGlobalConfig} | |
| // onChange={async (e) => { | |
| // const checked = e.currentTarget.checked; | |
| // if ( | |
| // checked && | |
| // (await showConfirm(Locale.Mask.Config.Sync.Confirm)) | |
| // ) { | |
| // props.updateMask((mask) => { | |
| // mask.syncGlobalConfig = checked; | |
| // mask.modelConfig = { ...globalConfig.modelConfig }; | |
| // }); | |
| // } else if (!checked) { | |
| // props.updateMask((mask) => { | |
| // mask.syncGlobalConfig = checked; | |
| // }); | |
| // } | |
| // }} | |
| // ></input> | |
| // </ListItem> | |
| // ) : null} | |
| // </List> | |
| // <List> | |
| // <ModelConfigList | |
| // modelConfig={{ ...props.mask.modelConfig }} | |
| // updateConfig={updateConfig} | |
| // /> | |
| // {props.extraListItems} | |
| // </List> | |
| // </> | |
| // ); | |
| // } | |
| function ContextPromptItem(props: { | |
| prompt: ChatMessage; | |
| update: (prompt: ChatMessage) => void; | |
| remove: () => void; | |
| }) { | |
| const [focusingInput, setFocusingInput] = useState(false); | |
| return ( | |
| <div className={chatStyle["context-prompt-row"]}> | |
| {!focusingInput && ( | |
| <Select | |
| value={props.prompt.role} | |
| className={chatStyle["context-role"]} | |
| onChange={(e) => | |
| props.update({ | |
| ...props.prompt, | |
| role: e.target.value as any, | |
| }) | |
| } | |
| > | |
| {ROLES.map((r) => ( | |
| <option key={r} value={r}> | |
| {r} | |
| </option> | |
| ))} | |
| </Select> | |
| )} | |
| <Input | |
| value={props.prompt.content} | |
| type="text" | |
| className={chatStyle["context-content"]} | |
| rows={focusingInput ? 5 : 1} | |
| onFocus={() => setFocusingInput(true)} | |
| onBlur={() => { | |
| setFocusingInput(false); | |
| // If the selection is not removed when the user loses focus, some | |
| // extensions like "Translate" will always display a floating bar | |
| window?.getSelection()?.removeAllRanges(); | |
| }} | |
| onInput={(e) => | |
| props.update({ | |
| ...props.prompt, | |
| content: e.currentTarget.value as any, | |
| }) | |
| } | |
| /> | |
| {!focusingInput && ( | |
| <IconButton | |
| icon={<DeleteIcon />} | |
| className={chatStyle["context-delete-button"]} | |
| onClick={() => props.remove()} | |
| bordered | |
| /> | |
| )} | |
| </div> | |
| ); | |
| } | |
| export function ContextPrompts(props: { | |
| context: ChatMessage[]; | |
| updateContext: (updater: (context: ChatMessage[]) => void) => void; | |
| }) { | |
| const context = props.context; | |
| const addContextPrompt = (prompt: ChatMessage) => { | |
| props.updateContext((context) => context.push(prompt)); | |
| }; | |
| const removeContextPrompt = (i: number) => { | |
| props.updateContext((context) => context.splice(i, 1)); | |
| }; | |
| const updateContextPrompt = (i: number, prompt: ChatMessage) => { | |
| props.updateContext((context) => (context[i] = prompt)); | |
| }; | |
| return ( | |
| <> | |
| <div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}> | |
| {context.map((c, i) => ( | |
| <ContextPromptItem | |
| key={i} | |
| prompt={c} | |
| update={(prompt) => updateContextPrompt(i, prompt)} | |
| remove={() => removeContextPrompt(i)} | |
| /> | |
| ))} | |
| <div className={chatStyle["context-prompt-row"]}> | |
| <IconButton | |
| icon={<AddIcon />} | |
| text={Locale.Context.Add} | |
| bordered | |
| className={chatStyle["context-prompt-button"]} | |
| onClick={() => | |
| addContextPrompt( | |
| createMessage({ | |
| role: "user", | |
| content: "", | |
| date: "", | |
| }), | |
| ) | |
| } | |
| /> | |
| </div> | |
| </div> | |
| </> | |
| ); | |
| } | |
| export function PluginPage() { | |
| const navigate = useNavigate(); | |
| const pluginStore = usePluginStore(); | |
| const chatStore = useChatStore(); | |
| const allPlugins = pluginStore | |
| .getAll() | |
| .filter( | |
| (m) => !getLang() || m.lang === (getLang() == "cn" ? getLang() : "en"), | |
| ); | |
| const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]); | |
| const [searchText, setSearchText] = useState(""); | |
| const plugins = searchText.length > 0 ? searchPlugins : allPlugins; | |
| // simple search, will refactor later | |
| const onSearch = (text: string) => { | |
| setSearchText(text); | |
| if (text.length > 0) { | |
| const result = allPlugins.filter((m) => m.name.includes(text)); | |
| setSearchPlugins(result); | |
| } else { | |
| setSearchPlugins(allPlugins); | |
| } | |
| }; | |
| const [editingPluginId, setEditingPluginId] = useState<string | undefined>(); | |
| const editingPlugin = | |
| pluginStore.get(editingPluginId) ?? | |
| BUILTIN_PLUGIN_STORE.get(editingPluginId); | |
| const closePluginModal = () => setEditingPluginId(undefined); | |
| const downloadAll = () => { | |
| downloadAs(JSON.stringify(plugins), FileName.Plugins); | |
| }; | |
| const updatePluginEnableStatus = (id: string, enable: boolean) => { | |
| console.log(enable); | |
| if (enable) pluginStore.enable(id); | |
| else pluginStore.disable(id); | |
| }; | |
| const importFromFile = () => { | |
| readFromFile().then((content) => { | |
| try { | |
| const importPlugins = JSON.parse(content); | |
| if (Array.isArray(importPlugins)) { | |
| for (const plugin of importPlugins) { | |
| if (plugin.name) { | |
| pluginStore.create(plugin); | |
| } | |
| } | |
| return; | |
| } | |
| if (importPlugins.name) { | |
| pluginStore.create(importPlugins); | |
| } | |
| } catch {} | |
| }); | |
| }; | |
| return ( | |
| <ErrorBoundary> | |
| <div className={styles["plugin-page"]}> | |
| <div className={styles["plugin-header"]}> | |
| <IconButton | |
| icon={<LeftIcon />} | |
| text={Locale.NewChat.Return} | |
| onClick={() => navigate(Path.Home)} | |
| ></IconButton> | |
| </div> | |
| <div className="window-header"> | |
| <div className="window-header-title"> | |
| <div className="window-header-main-title"> | |
| {Locale.Plugin.Page.Title} | |
| </div> | |
| <div className="window-header-submai-title"> | |
| {Locale.Plugin.Page.SubTitle(allPlugins.length)} | |
| </div> | |
| </div> | |
| <div className="window-actions"> | |
| {/* <div className="window-action-button"> | |
| <IconButton | |
| icon={<DownloadIcon />} | |
| bordered | |
| onClick={downloadAll} | |
| /> | |
| </div> | |
| <div className="window-action-button"> | |
| <IconButton | |
| icon={<UploadIcon />} | |
| bordered | |
| onClick={() => importFromFile()} | |
| /> | |
| </div> */} | |
| </div> | |
| </div> | |
| <div className={styles["plugin-page-body"]}> | |
| <div className={styles["plugin-filter"]}> | |
| <input | |
| type="text" | |
| className={styles["search-bar"]} | |
| placeholder={Locale.Plugin.Page.Search} | |
| autoFocus | |
| onInput={(e) => onSearch(e.currentTarget.value)} | |
| /> | |
| {/* <IconButton | |
| className={styles["mask-create"]} | |
| icon={<AddIcon />} | |
| text={Locale.Mask.Page.Create} | |
| bordered | |
| onClick={() => { | |
| const createdMask = pluginStore.create(); | |
| setEditingMaskId(createdMask.id); | |
| }} | |
| /> */} | |
| </div> | |
| <div> | |
| {plugins.map((m) => ( | |
| <div className={styles["plugin-item"]} key={m.id}> | |
| <div className={styles["plugin-header"]}> | |
| <div className={styles["plugin-title"]}> | |
| <div className={styles["plugin-name"]}>{m.name}</div> | |
| {/* 描述 */} | |
| <div className={styles["plugin-info"] + " one-line"}> | |
| {`${m.description}`} | |
| </div> | |
| </div> | |
| </div> | |
| <div className={styles["plugin-actions"]}> | |
| <input | |
| type="checkbox" | |
| checked={m.enable} | |
| onChange={(e) => { | |
| updatePluginEnableStatus(m.id, e.currentTarget.checked); | |
| }} | |
| ></input> | |
| {/* {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={async () => { | |
| if (await showConfirm(Locale.Mask.Item.DeleteConfirm)) { | |
| maskStore.delete(m.id); | |
| } | |
| }} | |
| /> | |
| )} */} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| {editingPlugin && ( | |
| <div className="modal-mask"> | |
| <Modal | |
| title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)} | |
| onClose={closePluginModal} | |
| actions={[ | |
| <IconButton | |
| icon={<DownloadIcon />} | |
| text={Locale.Plugin.EditModal.Download} | |
| key="export" | |
| bordered | |
| onClick={() => | |
| downloadAs( | |
| JSON.stringify(editingPlugin), | |
| `${editingPlugin.name}.json`, | |
| ) | |
| } | |
| />, | |
| ]} | |
| > | |
| {/* <PluginConfig | |
| plugin={editingPlugin} | |
| updatePlugin={(updater) => | |
| pluginStore.update(editingPluginId!, updater) | |
| } | |
| readonly={editingPlugin.builtin} | |
| /> */} | |
| </Modal> | |
| </div> | |
| )} | |
| </ErrorBoundary> | |
| ); | |
| } | |