diff --git a/app/components/Loading/index.module.scss b/app/components/Loading/index.module.scss new file mode 100644 index 000000000..98f568e29 --- /dev/null +++ b/app/components/Loading/index.module.scss @@ -0,0 +1,8 @@ +.loading-content { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + } \ No newline at end of file diff --git a/app/components/Loading/index.tsx b/app/components/Loading/index.tsx new file mode 100644 index 000000000..2ae0ef2a4 --- /dev/null +++ b/app/components/Loading/index.tsx @@ -0,0 +1,13 @@ +import BotIcon from "@/app/icons/bot.svg"; +import LoadingIcon from "@/app/icons/three-dots.svg"; + +import styles from "./index.module.scss"; + +export default function Loading(props: { noLogo?: boolean }) { + return ( +
+ {!props.noLogo && } + +
+ ); +} diff --git a/app/components/Popover/index.tsx b/app/components/Popover/index.tsx new file mode 100644 index 000000000..dcfb59c83 --- /dev/null +++ b/app/components/Popover/index.tsx @@ -0,0 +1,119 @@ +import { useState } from "react"; + +export default function Popover(props: { + content?: JSX.Element | string; + children?: JSX.Element; + show?: boolean; + onShow?: (v: boolean) => void; + className?: string; + popoverClassName?: string; + trigger?: "hover" | "click"; + placement?: "t" | "lt" | "rt" | "lb" | "rb" | "b"; + noArrow?: boolean; +}) { + const { + content, + children, + show, + onShow, + className, + popoverClassName, + trigger = "hover", + placement = "t", + noArrow = false, + } = props; + + const [internalShow, setShow] = useState(false); + + const mergedShow = show ?? internalShow; + + let placementClassName; + let arrowClassName = + "rotate-45 w-[8.5px] h-[8.5px] left-[50%] translate-x-[calc(-50%)] bg-black rounded-[1px] "; + + switch (placement) { + case "b": + placementClassName = + "bottom-[calc(-100%-0.5rem)] left-[50%] translate-x-[calc(-50%)]"; + arrowClassName += "bottom-[-5px] "; + break; + // case 'l': + // placementClassName = ''; + // break; + // case 'r': + // placementClassName = ''; + // break; + case "rb": + placementClassName = "bottom-[calc(-100%-0.5rem)]"; + arrowClassName += "bottom-[-5px] "; + break; + case "lt": + placementClassName = + "top-[calc(-100%-0.5rem)] left-[100%] translate-x-[calc(-100%)]"; + arrowClassName += "top-[-5px] "; + break; + case "lb": + placementClassName = + "bottom-[calc(-100%-0.5rem)] left-[100%] translate-x-[calc(-100%)]"; + arrowClassName += "bottom-[-5px] "; + break; + case "rt": + placementClassName = "top-[calc(-100%-0.5rem)]"; + arrowClassName += "top-[-5px] "; + break; + case "t": + default: + placementClassName = + "top-[calc(-100%-0.5rem)] left-[50%] translate-x-[calc(-50%)]"; + arrowClassName += "top-[-5px] "; + } + + const popoverCommonClass = "absolute p-2 box-border"; + + if (noArrow) { + arrowClassName = "hidden"; + } + + if (trigger === "click") { + return ( +
{ + e.preventDefault(); + onShow?.(!mergedShow); + setShow(!mergedShow); + }} + > + {children} + {mergedShow && ( + <> + {!noArrow && ( +
 
+ )} +
+ {content} +
+ + )} +
+ ); + } + + return ( +
+ {children} + {!noArrow && ( +
+   +
+ )} +
+ {content} +
+
+ ); +} diff --git a/app/components/Screen/index.tsx b/app/components/Screen/index.tsx new file mode 100644 index 000000000..46af6bfd3 --- /dev/null +++ b/app/components/Screen/index.tsx @@ -0,0 +1,71 @@ +import { useLocation } from "react-router-dom"; +import { useMemo, ReactNode, useLayoutEffect } from "react"; +import { DEFAULT_SIDEBAR_WIDTH, Path, SlotID } from "@/app/constant"; +import { getLang } from "@/app/locales"; + +import useMobileScreen from "@/app/hooks/useMobileScreen"; +import { isIOS } from "@/app/utils"; + +import backgroundUrl from "!url-loader!@/app/icons/background.svg"; +import useListenWinResize from "@/app/hooks/useListenWinResize"; + +interface ScreenProps { + children: ReactNode; + noAuth: ReactNode; + sidebar: ReactNode; +} + +export default function Screen(props: ScreenProps) { + const location = useLocation(); + const isAuth = location.pathname === Path.Auth; + const isHome = location.pathname === Path.Home; + + const isMobileScreen = useMobileScreen(); + const isIOSMobile = useMemo( + () => isIOS() && isMobileScreen, + [isMobileScreen], + ); + + useListenWinResize(); + + let containerClassName = "flex h-[100%] w-[100%]"; + let pageClassName = "flex-1 h-[100%]"; + let sidebarClassName = "basis-sidebar h-[100%]"; + + if (isMobileScreen) { + containerClassName = "h-[100%] w-[100%] relative bg-center"; + pageClassName = `absolute top-0 h-[100%] w-[100%] ${ + !isHome ? "left-0" : "left-[101%]" + } z-10`; + sidebarClassName = `h-[100%] w-[100%]`; + } + + return ( +
+ {isAuth ? ( + props.noAuth + ) : ( + <> +
{props.sidebar}
+ +
+ {props.children} +
+ + )} +
+ ); +} diff --git a/app/components/TabActions/index.tsx b/app/components/TabActions/index.tsx deleted file mode 100644 index 1e04c0270..000000000 --- a/app/components/TabActions/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { isValidElement } from "react"; - -type IconMap = { - active: JSX.Element; - inactive: JSX.Element; -}; -interface Action { - id: string; - icons: JSX.Element | IconMap; - className?: string; -} - -export interface TabActionsProps { - actionsShema: Action[]; - onSelect: (id: string) => void; - selected: string; - groups: string[][]; - className?: string; -} - -export default function TabActions(props: TabActionsProps) { - const { actionsShema, onSelect, selected, groups, className } = props; - - const content = groups.reduce((res, group, ind, arr) => { - res.push( - ...group.map((i) => { - const action = actionsShema.find((a) => a.id === i); - if (!action) { - return <>; - } - - const { icons } = action; - let activeIcon, inactiveIcon; - - if (isValidElement(icons)) { - activeIcon = icons; - inactiveIcon = icons; - } else { - activeIcon = (icons as IconMap).active; - inactiveIcon = (icons as IconMap).inactive; - } - - return ( -
{ - e.preventDefault(); - if (selected !== action.id) { - onSelect?.(action.id); - } - }} - > - {selected === action.id ? activeIcon : inactiveIcon} -
- ); - }), - ); - if (ind < arr.length - 1) { - res.push(
); - } - return res; - }, [] as JSX.Element[]); - - return ( -
{content}
- ); -} diff --git a/app/components/home.tsx b/app/components/home.tsx index d6dd63c9a..4749e25d9 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -30,7 +30,6 @@ import { getClientConfig } from "../config/client"; import { ClientApi } from "../client/api"; import { useAccessStore } from "../store"; import { identifyDefaultClaudeModel } from "../utils/checkers"; -import useMobileScreen from "@/app/hooks/useMobileScreen"; import backgroundUrl from "!url-loader!@/app/icons/background.svg"; export function Loading(props: { noLogo?: boolean }) { @@ -126,11 +125,9 @@ const loadAsyncGoogleFont = () => { }; function Screen() { - const config = useAppConfig(); const location = useLocation(); const isHome = location.pathname === Path.Home; const isAuth = location.pathname === Path.Auth; - const isMobileScreen = useMobileScreen(); useEffect(() => { loadAsyncGoogleFont(); @@ -155,7 +152,7 @@ function Screen() {
diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 1afd7de3b..ec3b89339 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -177,13 +177,14 @@ export function Markdown( fontSize?: number; parentRef?: RefObject; defaultShow?: boolean; + className?: string; } & React.DOMAttributes, ) { const mdRef = useRef(null); return (
void; @@ -34,8 +36,11 @@ export function ChatActions(props: { showPromptModal: () => void; scrollToBottom: () => void; showPromptHints: () => void; + showModelSelector: (show: boolean) => void; hitBottom: boolean; uploading: boolean; + isMobileScreen: boolean; + className?: string; }) { const config = useAppConfig(); const navigate = useNavigate(); @@ -62,7 +67,6 @@ export function ChatActions(props: { () => allModels.filter((m) => m.available), [allModels], ); - const [showModelSelector, setShowModelSelector] = useState(false); const [showUploadImage, setShowUploadImage] = useState(false); useEffect(() => { @@ -85,106 +89,159 @@ export function ChatActions(props: { } }, [chatStore, currentModel, models]); + const actions = [ + { + onClick: stopAll, + text: Locale.Chat.InputActions.Stop, + isShow: couldStop, + icon: , + placement: "left", + }, + { + onClick: props.scrollToBottom, + text: Locale.Chat.InputActions.ToBottom, + isShow: !props.hitBottom, + icon: , + placement: "left", + }, + { + onClick: props.showPromptModal, + text: Locale.Chat.InputActions.Settings, + isShow: props.hitBottom, + icon: , + placement: "right", + }, + { + onClick: props.uploadImage, + text: Locale.Chat.InputActions.UploadImage, + isShow: showUploadImage, + icon: props.uploading ? : , + placement: "left", + }, + { + onClick: nextTheme, + text: Locale.Chat.InputActions.Theme[theme], + isShow: true, + icon: ( + <> + {theme === Theme.Auto ? ( + + ) : theme === Theme.Light ? ( + + ) : theme === Theme.Dark ? ( + + ) : null} + + ), + placement: "left", + }, + { + onClick: props.showPromptHints, + text: Locale.Chat.InputActions.Prompt, + isShow: true, + icon: , + placement: "left", + }, + { + onClick: () => { + navigate(Path.Masks); + }, + text: Locale.Chat.InputActions.Masks, + isShow: true, + icon: , + placement: "left", + }, + { + onClick: () => { + chatStore.updateCurrentSession((session) => { + if (session.clearContextIndex === session.messages.length) { + session.clearContextIndex = undefined; + } else { + session.clearContextIndex = session.messages.length; + session.memoryPrompt = ""; // will clear memory + } + }); + }, + text: Locale.Chat.InputActions.Clear, + isShow: true, + icon: , + placement: "right", + }, + { + onClick: () => props.showModelSelector(true), + text: currentModel, + isShow: true, + icon: , + placement: "left", + }, + ] as const; + + if (props.isMobileScreen) { + const content = ( +
+ {actions.map((act) => { + return ( +
+ {act.icon} + {act.text} +
+ ); + })} +
+ ); + return ( + + + + ); + } + + const popoverClassName = `bg-gray-800 whitespace-nowrap px-3 py-2.5 text-white text-sm-title rounded-md`; + return ( -
- {couldStop && ( - } - /> - )} - {!props.hitBottom && ( - } - /> - )} - {props.hitBottom && ( - } - /> - )} - - {showUploadImage && ( - : } - /> - )} - - {theme === Theme.Auto ? ( - - ) : theme === Theme.Light ? ( - - ) : theme === Theme.Dark ? ( - - ) : null} - - } - /> - - } - /> - - { - navigate(Path.Masks); - }} - text={Locale.Chat.InputActions.Masks} - icon={} - /> - - } - onClick={() => { - chatStore.updateCurrentSession((session) => { - if (session.clearContextIndex === session.messages.length) { - session.clearContextIndex = undefined; - } else { - session.clearContextIndex = session.messages.length; - session.memoryPrompt = ""; // will clear memory - } - }); - }} - /> - - setShowModelSelector(true)} - text={currentModel} - icon={} - /> - - {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]); - }} - /> - )} +
+ {actions + .filter((v) => v.placement === "left" && v.isShow) + .map((act, ind) => { + return ( + +
+ {act.icon} +
+
+ ); + })} +
+ {actions + .filter((v) => v.placement === "right" && v.isShow) + .map((act, ind, arr) => { + return ( + +
+ {act.icon} +
+
+ ); + })}
); } diff --git a/app/containers/Chat/ChatHeader.tsx b/app/containers/Chat/ChatHeader.tsx index ac21afc37..f99152559 100644 --- a/app/containers/Chat/ChatHeader.tsx +++ b/app/containers/Chat/ChatHeader.tsx @@ -1,75 +1,88 @@ import { useNavigate } from "react-router-dom"; -import { IconButton } from "@/app/components/button"; import Locale from "@/app/locales"; import { Path } from "@/app/constant"; import { DEFAULT_TOPIC, useChatStore } from "@/app/store/chat"; -import RenameIcon from "@/app/icons/rename.svg"; -import ExportIcon from "@/app/icons/share.svg"; -import ReturnIcon from "@/app/icons/return.svg"; - -import styles from "./index.module.scss"; +import LogIcon from "@/app/icons/logIcon.svg"; +import GobackIcon from "@/app/icons/goback.svg"; +import ShareIcon from "@/app/icons/shareIcon.svg"; +import BottomArrow from "@/app/icons/bottomArrow.svg"; export interface ChatHeaderProps { isMobileScreen: boolean; setIsEditingMessage: (v: boolean) => void; setShowExport: (v: boolean) => void; + showModelSelector: (v: boolean) => void; } export default function ChatHeader(props: ChatHeaderProps) { - const { isMobileScreen, setIsEditingMessage, setShowExport } = props; + const { + isMobileScreen, + setIsEditingMessage, + setShowExport, + showModelSelector, + } = props; const navigate = useNavigate(); const chatStore = useChatStore(); const session = chatStore.currentSession(); + const currentModel = chatStore.currentSession().mask.modelConfig.model; + + let containerClassName = ""; + let titleClassName = "mr-4"; + let mainTitleClassName = ""; + let subTitleClassName = ""; + + if (isMobileScreen) { + containerClassName = "h-menu-title-mobile"; + titleClassName = "flex flex-col items-center justify-center gap-0.5 text"; + mainTitleClassName = "text-sm-title h-[19px] leading-5"; + subTitleClassName = "text-sm-mobile-tab leading-4"; + } + return ( -
- {isMobileScreen && ( -
-
- } - bordered - title={Locale.Chat.Actions.ChatList} - onClick={() => navigate(Path.Home)} - /> -
+
+ {isMobileScreen ? ( +
navigate(Path.Home)}> +
+ ) : ( + )} -
+
setIsEditingMessage(true)} > {!session.topic ? DEFAULT_TOPIC : session.topic}
-
- {Locale.Chat.SubTitle(session.messages.length)} +
+ {isMobileScreen ? ( +
showModelSelector(true)} + > + {currentModel} + +
+ ) : ( + Locale.Chat.SubTitle(session.messages.length) + )}
-
- {!isMobileScreen && ( -
- } - bordered - onClick={() => setIsEditingMessage(true)} - /> -
- )} -
- } - bordered - title={Locale.Chat.Actions.Export} - onClick={() => { - setShowExport(true); - }} - /> -
+ +
{ + setShowExport(true); + }} + > +
); diff --git a/app/containers/Chat/ChatInputPanel.tsx b/app/containers/Chat/ChatInputPanel.tsx index 09b35bbef..58638db6a 100644 --- a/app/containers/Chat/ChatInputPanel.tsx +++ b/app/containers/Chat/ChatInputPanel.tsx @@ -36,6 +36,7 @@ export interface ChatInputPanelProps { setIsLoading: (value: boolean) => void; setShowPromptModal: (value: boolean) => void; _setMsgRenderIndex: (value: number) => void; + showModelSelector: (value: boolean) => void; } export interface ChatInputPanelInstance { @@ -72,6 +73,7 @@ export default forwardRef( _setMsgRenderIndex, hitBottom, inputRows, + showModelSelector, } = props; const [uploading, setUploading] = useState(false); @@ -222,86 +224,107 @@ export default forwardRef( setUploading, }); + let inputClassName = " flex flex-col px-5 pb-5"; + let actionsClassName = "py-2.5"; + let inputTextAreaClassName = ""; + + if (isMobileScreen) { + inputClassName = "flex flex-row-reverse items-center gap-2 p-3"; + actionsClassName = ""; + inputTextAreaClassName = ""; + } + return ( -
- - - setShowPromptModal(true)} - scrollToBottom={scrollToBottom} - hitBottom={hitBottom} - uploading={uploading} - showPromptHints={() => { - // Click again to close - if (promptHints.length > 0) { - setPromptHints([]); - return; - } - - inputRef.current?.focus(); - setUserInput("/"); - onSearch(""); - }} +
+ -