Biomimicry-AI/Nexus
0
1import { IconButton } from "./button";2import { ErrorBoundary } from "./error";3 4import styles from "./plugin.module.scss";5 6import DownloadIcon from "../icons/download.svg";7import UploadIcon from "../icons/upload.svg";8import EditIcon from "../icons/edit.svg";9import AddIcon from "../icons/add.svg";10import CloseIcon from "../icons/close.svg";11import DeleteIcon from "../icons/delete.svg";12import EyeIcon from "../icons/eye.svg";13import CopyIcon from "../icons/copy.svg";14import LeftIcon from "../icons/left.svg";15 16import { Plugin, usePluginStore } from "../store/plugin";17import {18 ChatMessage,19 createMessage,20 ModelConfig,21 useAppConfig,22 useChatStore,23} from "../store";24import { ROLES } from "../client/api";25import {26 Input,27 List,28 ListItem,29 Modal,30 Popover,31 Select,32 showConfirm,33} from "./ui-lib";34import { Avatar, AvatarPicker } from "./emoji";35import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";36import { useLocation, useNavigate } from "react-router-dom";37 38import chatStyle from "./chat.module.scss";39import { useEffect, useState } from "react";40import { copyToClipboard, downloadAs, readFromFile } from "../utils";41import { Updater } from "../typing";42import { ModelConfigList } from "./model-config";43import { FileName, Path } from "../constant";44import { BUILTIN_PLUGIN_STORE } from "../plugins";45import { nanoid } from "nanoid";46import { getISOLang, getLang } from "../locales";47 48// export function PluginConfig(props: {49// plugin: Plugin;50// updateMask: Updater<Plugin>;51// extraListItems?: JSX.Element;52// readonly?: boolean;53// shouldSyncFromGlobal?: boolean;54// }) {55// const [showPicker, setShowPicker] = useState(false);56 57// const updateConfig = (updater: (config: ModelConfig) => void) => {58// if (props.readonly) return;59 60// // const config = { ...props.mask.modelConfig };61// // updater(config);62// props.updateMask((mask) => {63// // mask.modelConfig = config;64// // // if user changed current session mask, it will disable auto sync65// // mask.syncGlobalConfig = false;66// });67// };68 69// const globalConfig = useAppConfig();70 71// return (72// <>73// <ContextPrompts74// context={props.mask.context}75// updateContext={(updater) => {76// const context = props.mask.context.slice();77// updater(context);78// props.updateMask((mask) => (mask.context = context));79// }}80// />81 82// <List>83// <ListItem title={Locale.Mask.Config.Avatar}>84// <Popover85// content={86// <AvatarPicker87// onEmojiClick={(emoji) => {88// props.updateMask((mask) => (mask.avatar = emoji));89// setShowPicker(false);90// }}91// ></AvatarPicker>92// }93// open={showPicker}94// onClose={() => setShowPicker(false)}95// >96// <div97// onClick={() => setShowPicker(true)}98// style={{ cursor: "pointer" }}99// >100// </div>101// </Popover>102// </ListItem>103// <ListItem title={Locale.Mask.Config.Name}>104// <input105// type="text"106// value={props.mask.name}107// onInput={(e) =>108// props.updateMask((mask) => {109// mask.name = e.currentTarget.value;110// })111// }112// ></input>113// </ListItem>114// <ListItem115// title={Locale.Mask.Config.HideContext.Title}116// subTitle={Locale.Mask.Config.HideContext.SubTitle}117// >118// <input119// type="checkbox"120// checked={props.mask.hideContext}121// onChange={(e) => {122// props.updateMask((mask) => {123// mask.hideContext = e.currentTarget.checked;124// });125// }}126// ></input>127// </ListItem>128 129// {!props.shouldSyncFromGlobal ? (130// <ListItem131// title={Locale.Mask.Config.Share.Title}132// subTitle={Locale.Mask.Config.Share.SubTitle}133// >134// <IconButton135// icon={<CopyIcon />}136// text={Locale.Mask.Config.Share.Action}137// onClick={copyMaskLink}138// />139// </ListItem>140// ) : null}141 142// {props.shouldSyncFromGlobal ? (143// <ListItem144// title={Locale.Mask.Config.Sync.Title}145// subTitle={Locale.Mask.Config.Sync.SubTitle}146// >147// <input148// type="checkbox"149// checked={props.mask.syncGlobalConfig}150// onChange={async (e) => {151// const checked = e.currentTarget.checked;152// if (153// checked &&154// (await showConfirm(Locale.Mask.Config.Sync.Confirm))155// ) {156// props.updateMask((mask) => {157// mask.syncGlobalConfig = checked;158// mask.modelConfig = { ...globalConfig.modelConfig };159// });160// } else if (!checked) {161// props.updateMask((mask) => {162// mask.syncGlobalConfig = checked;163// });164// }165// }}166// ></input>167// </ListItem>168// ) : null}169// </List>170 171// <List>172// <ModelConfigList173// modelConfig={{ ...props.mask.modelConfig }}174// updateConfig={updateConfig}175// />176// {props.extraListItems}177// </List>178// </>179// );180// }181 182function ContextPromptItem(props: {183 prompt: ChatMessage;184 update: (prompt: ChatMessage) => void;185 remove: () => void;186}) {187 const [focusingInput, setFocusingInput] = useState(false);188 189 return (190 <div className={chatStyle["context-prompt-row"]}>191 {!focusingInput && (192 <Select193 value={props.prompt.role}194 className={chatStyle["context-role"]}195 onChange={(e) =>196 props.update({197 ...props.prompt,198 role: e.target.value as any,199 })200 }201 >202 {ROLES.map((r) => (203 <option key={r} value={r}>204 {r}205 </option>206 ))}207 </Select>208 )}209 <Input210 value={props.prompt.content}211 type="text"212 className={chatStyle["context-content"]}213 rows={focusingInput ? 5 : 1}214 onFocus={() => setFocusingInput(true)}215 onBlur={() => {216 setFocusingInput(false);217 // If the selection is not removed when the user loses focus, some218 // extensions like "Translate" will always display a floating bar219 window?.getSelection()?.removeAllRanges();220 }}221 onInput={(e) =>222 props.update({223 ...props.prompt,224 content: e.currentTarget.value as any,225 })226 }227 />228 {!focusingInput && (229 <IconButton230 icon={<DeleteIcon />}231 className={chatStyle["context-delete-button"]}232 onClick={() => props.remove()}233 bordered234 />235 )}236 </div>237 );238}239 240export function ContextPrompts(props: {241 context: ChatMessage[];242 updateContext: (updater: (context: ChatMessage[]) => void) => void;243}) {244 const context = props.context;245 246 const addContextPrompt = (prompt: ChatMessage) => {247 props.updateContext((context) => context.push(prompt));248 };249 250 const removeContextPrompt = (i: number) => {251 props.updateContext((context) => context.splice(i, 1));252 };253 254 const updateContextPrompt = (i: number, prompt: ChatMessage) => {255 props.updateContext((context) => (context[i] = prompt));256 };257 258 return (259 <>260 <div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>261 {context.map((c, i) => (262 <ContextPromptItem263 key={i}264 prompt={c}265 update={(prompt) => updateContextPrompt(i, prompt)}266 remove={() => removeContextPrompt(i)}267 />268 ))}269 270 <div className={chatStyle["context-prompt-row"]}>271 <IconButton272 icon={<AddIcon />}273 text={Locale.Context.Add}274 bordered275 className={chatStyle["context-prompt-button"]}276 onClick={() =>277 addContextPrompt(278 createMessage({279 role: "user",280 content: "",281 date: "",282 }),283 )284 }285 />286 </div>287 </div>288 </>289 );290}291 292export function PluginPage() {293 const navigate = useNavigate();294 295 const pluginStore = usePluginStore();296 const chatStore = useChatStore();297 298 const allPlugins = pluginStore299 .getAll()300 .filter(301 (m) => !getLang() || m.lang === (getLang() == "cn" ? getLang() : "en"),302 );303 304 const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);305 const [searchText, setSearchText] = useState("");306 const plugins = searchText.length > 0 ? searchPlugins : allPlugins;307 308 // simple search, will refactor later309 const onSearch = (text: string) => {310 setSearchText(text);311 if (text.length > 0) {312 const result = allPlugins.filter((m) => m.name.includes(text));313 setSearchPlugins(result);314 } else {315 setSearchPlugins(allPlugins);316 }317 };318 319 const [editingPluginId, setEditingPluginId] = useState<string | undefined>();320 const editingPlugin =321 pluginStore.get(editingPluginId) ??322 BUILTIN_PLUGIN_STORE.get(editingPluginId);323 const closePluginModal = () => setEditingPluginId(undefined);324 325 const downloadAll = () => {326 downloadAs(JSON.stringify(plugins), FileName.Plugins);327 };328 329 const updatePluginEnableStatus = (id: string, enable: boolean) => {330 console.log(enable);331 if (enable) pluginStore.enable(id);332 else pluginStore.disable(id);333 };334 335 const importFromFile = () => {336 readFromFile().then((content) => {337 try {338 const importPlugins = JSON.parse(content);339 if (Array.isArray(importPlugins)) {340 for (const plugin of importPlugins) {341 if (plugin.name) {342 pluginStore.create(plugin);343 }344 }345 return;346 }347 if (importPlugins.name) {348 pluginStore.create(importPlugins);349 }350 } catch {}351 });352 };353 354 return (355 <ErrorBoundary>356 <div className={styles["plugin-page"]}>357 <div className={styles["plugin-header"]}>358 <IconButton359 icon={<LeftIcon />}360 text={Locale.NewChat.Return}361 onClick={() => navigate(Path.Home)}362 ></IconButton>363 </div>364 <div className="window-header">365 <div className="window-header-title">366 <div className="window-header-main-title">367 {Locale.Plugin.Page.Title}368 </div>369 <div className="window-header-submai-title">370 {Locale.Plugin.Page.SubTitle(allPlugins.length)}371 </div>372 </div>373 374 <div className="window-actions">375 {/* <div className="window-action-button">376 <IconButton377 icon={<DownloadIcon />}378 bordered379 onClick={downloadAll}380 />381 </div>382 <div className="window-action-button">383 <IconButton384 icon={<UploadIcon />}385 bordered386 onClick={() => importFromFile()}387 />388 </div> */}389 </div>390 </div>391 392 <div className={styles["plugin-page-body"]}>393 <div className={styles["plugin-filter"]}>394 <input395 type="text"396 className={styles["search-bar"]}397 placeholder={Locale.Plugin.Page.Search}398 autoFocus399 onInput={(e) => onSearch(e.currentTarget.value)}400 />401 402 {/* <IconButton403 className={styles["mask-create"]}404 icon={<AddIcon />}405 text={Locale.Mask.Page.Create}406 bordered407 onClick={() => {408 const createdMask = pluginStore.create();409 setEditingMaskId(createdMask.id);410 }}411 /> */}412 </div>413 414 <div>415 {plugins.map((m) => (416 <div className={styles["plugin-item"]} key={m.id}>417 <div className={styles["plugin-header"]}>418 <div className={styles["plugin-title"]}>419 <div className={styles["plugin-name"]}>{m.name}</div>420 {/* 描述 */}421 <div className={styles["plugin-info"] + " one-line"}>422 {`${m.description}`}423 </div>424 </div>425 </div>426 <div className={styles["plugin-actions"]}>427 <input428 type="checkbox"429 checked={m.enable}430 onChange={(e) => {431 updatePluginEnableStatus(m.id, e.currentTarget.checked);432 }}433 ></input>434 {/* {m.builtin ? (435 <IconButton436 icon={<EyeIcon />}437 text={Locale.Mask.Item.View}438 onClick={() => setEditingMaskId(m.id)}439 />440 ) : (441 <IconButton442 icon={<EditIcon />}443 text={Locale.Mask.Item.Edit}444 onClick={() => setEditingMaskId(m.id)}445 />446 )}447 {!m.builtin && (448 <IconButton449 icon={<DeleteIcon />}450 text={Locale.Mask.Item.Delete}451 onClick={async () => {452 if (await showConfirm(Locale.Mask.Item.DeleteConfirm)) {453 maskStore.delete(m.id);454 }455 }}456 />457 )} */}458 </div>459 </div>460 ))}461 </div>462 </div>463 </div>464 465 {editingPlugin && (466 <div className="modal-mask">467 <Modal468 title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)}469 onClose={closePluginModal}470 actions={[471 <IconButton472 icon={<DownloadIcon />}473 text={Locale.Plugin.EditModal.Download}474 key="export"475 bordered476 onClick={() =>477 downloadAs(478 JSON.stringify(editingPlugin),479 `${editingPlugin.name}.json`,480 )481 }482 />,483 ]}484 >485 {/* <PluginConfig486 plugin={editingPlugin}487 updatePlugin={(updater) =>488 pluginStore.update(editingPluginId!, updater)489 }490 readonly={editingPlugin.builtin}491 /> */}492 </Modal>493 </div>494 )}495 </ErrorBoundary>496 );497}498 