From 8c28c408d872fbfc54e3ff151cad1def95f34a5c Mon Sep 17 00:00:00 2001 From: butterfly Date: Mon, 29 Apr 2024 16:29:47 +0800 Subject: [PATCH] feat: refactor select model --- app/components/Btn/index.tsx | 6 +- app/components/Confirm/index.tsx | 136 --------- app/components/Input/index.tsx | 4 +- app/components/Modal/index.tsx | 261 ++++++++++++++++++ app/components/Popover/index.tsx | 21 +- app/components/Select/index.tsx | 18 +- app/containers/Chat/ChatPanel.tsx | 39 --- .../Chat/components/ChatActions.tsx | 27 +- app/containers/Chat/components/ChatHeader.tsx | 10 +- .../Chat/components/ChatInputPanel.tsx | 3 - .../Chat/components/ModelSelect.tsx | 147 ++++++++++ app/containers/Chat/index.tsx | 11 +- .../Settings/components/AppSetting.tsx | 12 +- app/icons/closeIcon.svg | 3 + app/icons/deleteIcon.svg | 2 +- app/icons/selectedIcon.svg | 4 +- app/styles/globals.css | 18 +- tailwind.config.js | 18 +- 18 files changed, 501 insertions(+), 239 deletions(-) delete mode 100644 app/components/Confirm/index.tsx create mode 100644 app/components/Modal/index.tsx create mode 100644 app/containers/Chat/components/ModelSelect.tsx create mode 100644 app/icons/closeIcon.svg diff --git a/app/components/Btn/index.tsx b/app/components/Btn/index.tsx index bb3898adb..75b70802c 100644 --- a/app/components/Btn/index.tsx +++ b/app/components/Btn/index.tsx @@ -2,7 +2,7 @@ import * as React from "react"; export type ButtonType = "primary" | "danger" | null; -export default function Btn(props: { +export interface BtnProps { onClick?: () => void; icon?: JSX.Element; type?: ButtonType; @@ -14,7 +14,9 @@ export default function Btn(props: { disabled?: boolean; tabIndex?: number; autoFocus?: boolean; -}) { +} + +export default function Btn(props: BtnProps) { const { onClick, icon, diff --git a/app/components/Confirm/index.tsx b/app/components/Confirm/index.tsx deleted file mode 100644 index e9d957871..000000000 --- a/app/components/Confirm/index.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React, { useState } from "react"; -import { createRoot } from "react-dom/client"; -import * as AlertDialog from "@radix-ui/react-alert-dialog"; -import Warning from "@/app/icons/warning.svg"; -import Btn from "@/app/components/Btn"; - -interface ConfirmProps { - onOk?: () => Promise | void; - onCancel?: () => void; - okText: string; - cancelText: string; - content: React.ReactNode; - title: React.ReactNode; - visible?: boolean; -} - -const baseZIndex = 150; - -const Confirm = (props: ConfirmProps) => { - const { visible, onOk, onCancel, okText, cancelText, content, title } = props; - - const [open, setOpen] = useState(false); - - const mergeOpen = visible ?? open; - - return ( - - - - -
 
-
- - - {title} - - - {content} - -
- - { - setOpen(false); - onCancel?.(); - }} - text={cancelText} - /> - - - { - const toDo = onOk?.(); - if (toDo instanceof Promise) { - toDo.then(() => { - setOpen(false); - }); - } else { - setOpen(false); - } - }} - text={okText} - /> - -
-
-
 
-
-
-
- ); -}; - -const div = document.createElement("div"); -div.id = "confirm-root"; -div.style.height = "0px"; -document.body.appendChild(div); - -const show = (props: Omit) => { - const root = createRoot(div); - const closeModal = () => { - root.unmount(); - }; - - return new Promise((resolve) => { - root.render( - { - closeModal(); - resolve(false); - }} - onOk={() => { - closeModal(); - resolve(true); - }} - />, - ); - }); -}; - -Confirm.show = show; - -export default Confirm; diff --git a/app/components/Input/index.tsx b/app/components/Input/index.tsx index 94ff37a89..4ab94f4f3 100644 --- a/app/components/Input/index.tsx +++ b/app/components/Input/index.tsx @@ -55,11 +55,11 @@ export default function Input(props: CommonInputProps & InputProps) { return (
{ diff --git a/app/components/Modal/index.tsx b/app/components/Modal/index.tsx new file mode 100644 index 000000000..9aea50510 --- /dev/null +++ b/app/components/Modal/index.tsx @@ -0,0 +1,261 @@ +import React, { useState } from "react"; +import { createRoot } from "react-dom/client"; +import * as AlertDialog from "@radix-ui/react-alert-dialog"; +import Btn, { BtnProps } from "@/app/components/Btn"; + +import Warning from "@/app/icons/warning.svg"; +import Close from "@/app/icons/closeIcon.svg"; + +export interface ModalProps { + onOk?: () => void; + onCancel?: () => void; + okText?: string; + cancelText?: string; + okBtnProps?: BtnProps; + cancelBtnProps?: BtnProps; + content?: React.ReactNode; + title?: React.ReactNode; + visible?: boolean; + noFooter?: boolean; + noHeader?: boolean; + isMobile?: boolean; + closeble?: boolean; + type?: "modal" | "bottom-drawer"; + headerBordered?: boolean; +} + +export interface WarnProps + extends Omit< + ModalProps, + | "closeble" + | "isMobile" + | "noHeader" + | "noFooter" + | "onOk" + | "okBtnProps" + | "cancelBtnProps" + > { + onOk?: () => Promise | void; +} + +const baseZIndex = 150; + +const Modal = (props: ModalProps) => { + const { + onOk, + onCancel, + okText, + cancelText, + content, + title, + visible, + noFooter, + noHeader, + closeble = true, + okBtnProps, + cancelBtnProps, + type = "modal", + headerBordered, + } = props; + + const [open, setOpen] = useState(false); + + const mergeOpen = visible ?? open; + + const handleClose = () => { + setOpen(false); + onCancel?.(); + }; + + let layoutClassName = ""; + let panelClassName = ""; + let titleClassName = ""; + let footerClassName = ""; + + switch (type) { + case "bottom-drawer": + layoutClassName = + "fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%] md:"; + panelClassName = ""; + titleClassName = ""; + footerClassName = ""; + break; + case "modal": + default: + layoutClassName = + "fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%] max-sm:w-modal-modal-type-mobile"; + panelClassName = "rounded-lg px-6 sm:w-modal-modal-type"; + titleClassName = "py-6 max-sm:pb-3"; + footerClassName = "py-6"; + } + const btnCommonClass = "px-4 py-2.5 rounded-md max-sm:flex-1"; + const { className: okBtnClass } = okBtnProps || {}; + const { className: cancelBtnClass } = cancelBtnProps || {}; + + return ( + + + + +
 
+
+ {!noHeader && ( + +
+ {title} +
+ {closeble && ( +
{ + handleClose(); + }} + > + +
+ )} +
+ )} + {content} + {!noFooter && ( +
+ + handleClose()} + text={cancelText} + className={`${btnCommonClass} ${cancelBtnClass}`} + /> + + + { + setOpen(false); + onOk?.(); + }} + text={okText} + className={`${btnCommonClass} ${okBtnClass}`} + /> + +
+ )} +
+
 
+
+
+
+ ); +}; + +export const Warn = ({ + title, + onOk, + visible, + content, + ...props +}: WarnProps) => { + const [internalVisible, setVisible] = useState(visible); + + return ( + + + {title} + + } + content={ + + {content} + + } + closeble={false} + onOk={() => { + const toDo = onOk?.(); + if (toDo instanceof Promise) { + toDo.then(() => { + setVisible(false); + }); + } else { + setVisible(false); + } + }} + visible={internalVisible} + okBtnProps={{ + className: `bg-delete-chat-ok-btn text-text-delete-chat-ok-btn `, + }} + cancelBtnProps={{ + className: `bg-delete-chat-cancel-btn border border-delete-chat-cancel-btn text-text-delete-chat-cancel-btn`, + }} + /> + ); +}; + +const div = document.createElement("div"); +div.id = "confirm-root"; +div.style.height = "0px"; +document.body.appendChild(div); + +Modal.warn = (props: Omit) => { + const root = createRoot(div); + const closeModal = () => { + root.unmount(); + }; + + return new Promise((resolve) => { + root.render( + { + closeModal(); + resolve(false); + }} + onOk={() => { + closeModal(); + resolve(true); + }} + />, + ); + }); +}; + +const Trigger = (props: ModalProps) => { + return <>; +}; + +Modal.Trigger = Trigger; + +export default Modal; diff --git a/app/components/Popover/index.tsx b/app/components/Popover/index.tsx index 15014c4f0..2baebc143 100644 --- a/app/components/Popover/index.tsx +++ b/app/components/Popover/index.tsx @@ -1,5 +1,12 @@ import useRelativePosition from "@/app/hooks/useRelativePosition"; -import { RefObject, useEffect, useMemo, useRef, useState } from "react"; +import { + RefObject, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; import { createPortal } from "react-dom"; const ArrowIcon = ({ sibling }: { sibling: RefObject }) => { @@ -55,6 +62,7 @@ export interface PopoverProps { noArrow?: boolean; delayClose?: number; useGlobalRoot?: boolean; + getPopoverPanelRef?: (ref: RefObject) => void; } export default function Popover(props: PopoverProps) { @@ -70,6 +78,7 @@ export default function Popover(props: PopoverProps) { noArrow = false, delayClose = 0, useGlobalRoot, + getPopoverPanelRef, } = props; const [internalShow, setShow] = useState(false); @@ -175,10 +184,14 @@ export default function Popover(props: PopoverProps) { const popoverRef = useRef(null); const closeTimer = useRef(0); + useLayoutEffect(() => { + getPopoverPanelRef?.(popoverRef); + onShow?.(internalShow); + }, [internalShow]); + if (trigger === "click") { const handleOpen = (e: { currentTarget: any }) => { clearTimeout(closeTimer.current); - onShow?.(true); setShow(true); getRelativePosition(e.currentTarget, ""); window.document.documentElement.style.overflow = "hidden"; @@ -186,11 +199,9 @@ export default function Popover(props: PopoverProps) { const handleClose = () => { if (delayClose) { closeTimer.current = window.setTimeout(() => { - onShow?.(false); setShow(false); }, delayClose); } else { - onShow?.(false); setShow(false); } window.document.documentElement.style.overflow = "auto"; @@ -219,7 +230,7 @@ export default function Popover(props: PopoverProps) { )} {createPortal(
diff --git a/app/components/Select/index.tsx b/app/components/Select/index.tsx index 15a14d1d6..fe8581b77 100644 --- a/app/components/Select/index.tsx +++ b/app/components/Select/index.tsx @@ -58,14 +58,22 @@ const Select = (props: SearchProps) => { {options?.map((o) => (
{ onSelect?.(o.value); }} > - {!!o.icon &&
{o.icon}
} -
{o.label}
- {selectedOption?.value === o.value && } +
+ {!!o.icon &&
{o.icon}
} +
{o.label}
+
+
+ +
))}
@@ -86,7 +94,7 @@ const Select = (props: SearchProps) => { className={selectClassName} >
diff --git a/app/containers/Chat/ChatPanel.tsx b/app/containers/Chat/ChatPanel.tsx index 5d4839c15..833e41bfa 100644 --- a/app/containers/Chat/ChatPanel.tsx +++ b/app/containers/Chat/ChatPanel.tsx @@ -38,7 +38,6 @@ function _Chat() { const { isMobileScreen } = config; const [showExport, setShowExport] = useState(false); - const [showModelSelector, setShowModelSelector] = useState(false); const inputRef = useRef(null); const [userInput, setUserInput] = useState(""); @@ -234,7 +233,6 @@ function _Chat() { setIsLoading, showChatSetting: setShowPromptModal, _setMsgRenderIndex, - showModelSelector: setShowModelSelector, scrollDomToBottom, setAutoScroll, }; @@ -256,23 +254,6 @@ function _Chat() { scrollDomToBottom, }; - const currentModel = chatStore.currentSession().mask.modelConfig.model; - const allModels = useAllModels(); - const models = useMemo(() => { - const filteredModels = allModels.filter((m) => m.available); - const defaultModel = filteredModels.find((m) => m.isDefault); - - if (defaultModel) { - const arr = [ - defaultModel, - ...filteredModels.filter((m) => m !== defaultModel), - ]; - return arr; - } else { - return filteredModels; - } - }, [allModels]); - return (
@@ -310,25 +290,6 @@ function _Chat() { {showPromptModal && ( setShowPromptModal(false)} /> )} - - {showModelSelector && ( - ({ - title: m.displayName, - value: m.name, - }))} - onClose={() => setShowModelSelector(false)} - onSelection={(s) => { - if (s.length === 0) return; - chatStore.updateCurrentSession((session) => { - session.mask.modelConfig.model = s[0] as ModelType; - session.mask.syncGlobalConfig = false; - }); - showToast(s[0]); - }} - /> - )}
); } diff --git a/app/containers/Chat/components/ChatActions.tsx b/app/containers/Chat/components/ChatActions.tsx index 2ec9a8c53..f6bfeddfc 100644 --- a/app/containers/Chat/components/ChatActions.tsx +++ b/app/containers/Chat/components/ChatActions.tsx @@ -19,15 +19,15 @@ import BreakIcon from "@/app/icons/eraserIcon.svg"; import SettingsIcon from "@/app/icons/configIcon.svg"; import ImageIcon from "@/app/icons/uploadImgIcon.svg"; import AddCircleIcon from "@/app/icons/addCircle.svg"; -import BottomArrow from "@/app/icons/downArrowLgIcon.svg"; import Popover from "@/app/components/Popover"; +import ModelSelect from "./ModelSelect"; export interface Action { - onClick: () => void; + onClick?: () => void; text: string; isShow: boolean; - pcRender?: () => JSX.Element; + render?: (key: string) => JSX.Element; icon?: JSX.Element; placement: "left" | "right"; } @@ -39,7 +39,6 @@ export function ChatActions(props: { showChatSetting: () => void; scrollToBottom: () => void; showPromptHints: () => void; - showModelSelector: (show: boolean) => void; hitBottom: boolean; uploading: boolean; isMobileScreen: boolean; @@ -101,15 +100,9 @@ export function ChatActions(props: { placement: "left", }, { - onClick: () => props.showModelSelector(true), text: currentModel, - isShow: true, - pcRender: () => ( -
- {currentModel} - -
- ), + isShow: !props.isMobileScreen, + render: (key: string) => , placement: "left", }, { @@ -182,7 +175,7 @@ export function ChatActions(props: { icon: , placement: "right", }, - ] as const; + ]; if (props.isMobileScreen) { const content = ( @@ -226,12 +219,8 @@ export function ChatActions(props: { {actions .filter((v) => v.placement === "left" && v.isShow) .map((act, ind) => { - if (act.pcRender) { - return ( -
- {act.pcRender()} -
- ); + if (act.render) { + return act.render(act.text); } return ( void; setShowExport: (v: boolean) => void; - showModelSelector: (v: boolean) => void; } export default function ChatHeader(props: ChatHeaderProps) { - const { - isMobileScreen, - setIsEditingMessage, - setShowExport, - showModelSelector, - } = props; + const { isMobileScreen, setIsEditingMessage, setShowExport } = props; const navigate = useNavigate(); @@ -78,7 +72,7 @@ export default function ChatHeader(props: ChatHeaderProps) { {isMobileScreen ? (
showModelSelector(true)} + onClick={() => {}} > {currentModel} diff --git a/app/containers/Chat/components/ChatInputPanel.tsx b/app/containers/Chat/components/ChatInputPanel.tsx index 069a7f261..7d9829467 100644 --- a/app/containers/Chat/components/ChatInputPanel.tsx +++ b/app/containers/Chat/components/ChatInputPanel.tsx @@ -35,7 +35,6 @@ export interface ChatInputPanelProps { setIsLoading: (value: boolean) => void; showChatSetting: (value: boolean) => void; _setMsgRenderIndex: (value: number) => void; - showModelSelector: (value: boolean) => void; setAutoScroll: (value: boolean) => void; scrollDomToBottom: () => void; } @@ -64,7 +63,6 @@ export default forwardRef( _setMsgRenderIndex, hitBottom, inputRows, - showModelSelector, setAutoScroll, scrollDomToBottom, } = props; @@ -226,7 +224,6 @@ export default forwardRef( `} > { + const config = useAppConfig(); + const { isMobileScreen } = config; + const chatStore = useChatStore(); + const currentModel = chatStore.currentSession().mask.modelConfig.model; + const allModels = useAllModels(); + const models = useMemo(() => { + const filteredModels = allModels.filter((m) => m.available); + const defaultModel = filteredModels.find((m) => m.isDefault); + + if (defaultModel) { + const arr = [ + defaultModel, + ...filteredModels.filter((m) => m !== defaultModel), + ]; + return arr; + } else { + return filteredModels; + } + }, [allModels]); + + const rootRef = useRef(null); + + const { position, getRelativePosition } = useRelativePosition({ + delay: 0, + }); + + const contentRef = useMemo<{ current: HTMLDivElement | null }>(() => { + return { + current: null, + }; + }, []); + const selectedItemRef = useRef(null); + + const autoScrollToSelectedModal = () => { + window.setTimeout(() => { + const distanceToParent = selectedItemRef.current?.offsetTop || 0; + const childHeight = selectedItemRef.current?.offsetHeight || 0; + const parentHeight = contentRef.current?.offsetHeight || 0; + const distanceToParentCenter = + distanceToParent + childHeight / 2 - parentHeight / 2; + + if (distanceToParentCenter > 0 && contentRef.current) { + contentRef.current.scrollTop = distanceToParentCenter; + } + }); + }; + + const content = ( +
+ {models?.map((o) => ( +
{ + chatStore.updateCurrentSession((session) => { + session.mask.modelConfig.model = o.name as ModelType; + session.mask.syncGlobalConfig = false; + }); + showToast(o.name); + }} + ref={currentModel === o.name ? selectedItemRef : undefined} + > +
{o.name}
+
+ +
+
+ ))} +
+ ); + + if (isMobileScreen) { + return ( + { + if (e) { + autoScrollToSelectedModal(); + getRelativePosition(rootRef.current!, ""); + } + }} + getPopoverPanelRef={(ref) => (contentRef.current = ref.current)} + > +
+ {currentModel} + +
+
+ ); + } + + return ( + { + if (e) { + autoScrollToSelectedModal(); + getRelativePosition(rootRef.current!, ""); + } + }} + getPopoverPanelRef={(ref) => (contentRef.current = ref.current)} + > +
+
+ {currentModel} +
+ +
+
+ ); +}; + +export default ModelSelect; diff --git a/app/containers/Chat/index.tsx b/app/containers/Chat/index.tsx index 1b52238e6..0cc705119 100644 --- a/app/containers/Chat/index.tsx +++ b/app/containers/Chat/index.tsx @@ -23,7 +23,7 @@ import LogIcon from "@/app/icons/logIcon.svg"; import MenuLayout from "@/app/components/MenuLayout"; import Panel from "./ChatPanel"; -import Confirm from "@/app/components/Confirm"; +import Modal from "@/app/components/Modal"; import HoverPopover from "@/app/components/HoverPopover"; export function SessionItem(props: { @@ -119,7 +119,12 @@ export function SessionItem(props: { align={props.isMobileScreen ? "end" : "start"} >
@@ -239,7 +244,7 @@ export default MenuLayout(function SessionList(props) { }} onDelete={async () => { if ( - await Confirm.show({ + await Modal.warn({ okText: Locale.ChatItem.DeleteOkBtn, cancelText: Locale.ChatItem.DeleteCancelBtn, title: Locale.ChatItem.DeleteTitle, diff --git a/app/containers/Settings/components/AppSetting.tsx b/app/containers/Settings/components/AppSetting.tsx index 89ff49d06..20e37854e 100644 --- a/app/containers/Settings/components/AppSetting.tsx +++ b/app/containers/Settings/components/AppSetting.tsx @@ -6,7 +6,12 @@ import styles from "../index.module.scss"; import { useEffect, useState } from "react"; import { Avatar, AvatarPicker } from "@/app/components/emoji"; import { Popover } from "@/app/components/ui-lib"; -import Locale, { AllLangs, changeLang, getLang } from "@/app/locales"; +import Locale, { + ALL_LANG_OPTIONS, + AllLangs, + changeLang, + getLang, +} from "@/app/locales"; import Link from "next/link"; import { IconButton } from "@/app/components/button"; import { useUpdateStore } from "@/app/store/update"; @@ -140,7 +145,10 @@ export default function AppSetting(props: AppSettingProps) {