import { IconButton } from "./button"; import { ErrorBoundary } from "./error"; import styles from "./mask.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 { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask"; import { Message, ModelConfig, ROLES, useChatStore } from "../store"; import { Input, List, ListItem, Modal, Popover, Select } from "./ui-lib"; import { Avatar, AvatarPicker } from "./emoji"; import Locale, { AllLangs, Lang } from "../locales"; import { useNavigate } from "react-router-dom"; import chatStyle from "./chat.module.scss"; import { useState } from "react"; import { downloadAs, readFromFile } from "../utils"; import { Updater } from "../api/openai/typing"; import { ModelConfigList } from "./model-config"; import { FileName, Path } from "../constant"; import { BUILTIN_MASK_STORE } from "../masks"; export function MaskAvatar(props: { mask: Mask }) { return props.mask.avatar !== DEFAULT_MASK_AVATAR ? ( ) : ( ); } export function MaskConfig(props: { mask: Mask; updateMask: Updater; extraListItems?: JSX.Element; readonly?: 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)); }; return ( <> { const context = props.mask.context.slice(); updater(context); props.updateMask((mask) => (mask.context = context)); }} /> { props.updateMask((mask) => (mask.avatar = emoji)); setShowPicker(false); }} > } open={showPicker} onClose={() => setShowPicker(false)} >
setShowPicker(true)} style={{ cursor: "pointer" }} >
props.updateMask((mask) => (mask.name = e.currentTarget.value)) } >
{props.extraListItems} ); } function ContextPromptItem(props: { prompt: Message; update: (prompt: Message) => void; remove: () => void; }) { const [focusingInput, setFocusingInput] = useState(false); return (
{!focusingInput && ( )} setFocusingInput(true)} onBlur={() => setFocusingInput(false)} onInput={(e) => props.update({ ...props.prompt, content: e.currentTarget.value as any, }) } /> {!focusingInput && ( } className={chatStyle["context-delete-button"]} onClick={() => props.remove()} bordered /> )}
); } export function ContextPrompts(props: { context: Message[]; updateContext: (updater: (context: Message[]) => void) => void; }) { const context = props.context; const addContextPrompt = (prompt: Message) => { props.updateContext((context) => context.push(prompt)); }; const removeContextPrompt = (i: number) => { props.updateContext((context) => context.splice(i, 1)); }; const updateContextPrompt = (i: number, prompt: Message) => { props.updateContext((context) => (context[i] = prompt)); }; return ( <>
{context.map((c, i) => ( updateContextPrompt(i, prompt)} remove={() => removeContextPrompt(i)} /> ))}
} text={Locale.Context.Add} bordered className={chatStyle["context-prompt-button"]} onClick={() => addContextPrompt({ role: "user", content: "", date: "", }) } />
); } export function MaskPage() { const navigate = useNavigate(); const maskStore = useMaskStore(); const chatStore = useChatStore(); const [filterLang, setFilterLang] = useState(); const allMasks = maskStore .getAll() .filter((m) => !filterLang || m.lang === filterLang); const [searchMasks, setSearchMasks] = useState([]); const [searchText, setSearchText] = useState(""); const masks = searchText.length > 0 ? searchMasks : allMasks; // simple search, will refactor later const onSearch = (text: string) => { setSearchText(text); if (text.length > 0) { const result = allMasks.filter((m) => m.name.includes(text)); setSearchMasks(result); } else { setSearchMasks(allMasks); } }; const [editingMaskId, setEditingMaskId] = useState(); const editingMask = maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId); const closeMaskModal = () => setEditingMaskId(undefined); const downloadAll = () => { downloadAs(JSON.stringify(masks), FileName.Masks); }; const importFromFile = () => { readFromFile().then((content) => { try { const importMasks = JSON.parse(content); if (Array.isArray(importMasks)) { for (const mask of importMasks) { if (mask.name) { maskStore.create(mask); } } } } catch {} }); }; return (
{Locale.Mask.Page.Title}
{Locale.Mask.Page.SubTitle(allMasks.length)}
} bordered onClick={downloadAll} />
} bordered onClick={() => importFromFile()} />
} bordered onClick={() => navigate(-1)} />
onSearch(e.currentTarget.value)} /> } text={Locale.Mask.Page.Create} bordered onClick={() => { const createdMask = maskStore.create(); setEditingMaskId(createdMask.id); }} />
{masks.map((m) => (
{m.name}
{`${Locale.Mask.Item.Info(m.context.length)} / ${ Locale.Settings.Lang.Options[m.lang] } / ${m.modelConfig.model}`}
} text={Locale.Mask.Item.Chat} onClick={() => { chatStore.newSession(m); navigate(Path.Chat); }} /> {m.builtin ? ( } text={Locale.Mask.Item.View} onClick={() => setEditingMaskId(m.id)} /> ) : ( } text={Locale.Mask.Item.Edit} onClick={() => setEditingMaskId(m.id)} /> )} {!m.builtin && ( } text={Locale.Mask.Item.Delete} onClick={() => { if (confirm(Locale.Mask.Item.DeleteConfirm)) { maskStore.delete(m.id); } }} /> )}
))}
{editingMask && (
} text={Locale.Mask.EditModal.Download} key="export" bordered onClick={() => downloadAs( JSON.stringify(editingMask), `${editingMask.name}.json`, ) } />, } bordered text={Locale.Mask.EditModal.Clone} onClick={() => { navigate(Path.Masks); maskStore.create(editingMask); setEditingMaskId(undefined); }} />, ]} > maskStore.update(editingMaskId!, updater) } readonly={editingMask.builtin} />
)}
); }