feat: select model done

This commit is contained in:
butterfly 2024-04-29 20:37:27 +08:00
parent 8c28c408d8
commit 5ea6206319
9 changed files with 242 additions and 165 deletions

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useLayoutEffect, useState } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import * as AlertDialog from "@radix-ui/react-alert-dialog"; import * as AlertDialog from "@radix-ui/react-alert-dialog";
import Btn, { BtnProps } from "@/app/components/Btn"; import Btn, { BtnProps } from "@/app/components/Btn";
@ -13,7 +13,9 @@ export interface ModalProps {
cancelText?: string; cancelText?: string;
okBtnProps?: BtnProps; okBtnProps?: BtnProps;
cancelBtnProps?: BtnProps; cancelBtnProps?: BtnProps;
content?: React.ReactNode; content?:
| React.ReactNode
| ((handlers: { close: () => void }) => JSX.Element);
title?: React.ReactNode; title?: React.ReactNode;
visible?: boolean; visible?: boolean;
noFooter?: boolean; noFooter?: boolean;
@ -22,6 +24,8 @@ export interface ModalProps {
closeble?: boolean; closeble?: boolean;
type?: "modal" | "bottom-drawer"; type?: "modal" | "bottom-drawer";
headerBordered?: boolean; headerBordered?: boolean;
modelClassName?: string;
onOpen?: (v: boolean) => void;
} }
export interface WarnProps export interface WarnProps
@ -34,8 +38,16 @@ export interface WarnProps
| "onOk" | "onOk"
| "okBtnProps" | "okBtnProps"
| "cancelBtnProps" | "cancelBtnProps"
| "content"
> { > {
onOk?: () => Promise<void> | void; onOk?: () => Promise<void> | void;
content?: React.ReactNode;
}
export interface TriggerProps
extends Omit<ModalProps, "visible" | "onOk" | "onCancel"> {
children: JSX.Element;
className?: string;
} }
const baseZIndex = 150; const baseZIndex = 150;
@ -56,9 +68,11 @@ const Modal = (props: ModalProps) => {
cancelBtnProps, cancelBtnProps,
type = "modal", type = "modal",
headerBordered, headerBordered,
modelClassName,
onOpen,
} = props; } = props;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(!!visible);
const mergeOpen = visible ?? open; const mergeOpen = visible ?? open;
@ -67,6 +81,15 @@ const Modal = (props: ModalProps) => {
onCancel?.(); onCancel?.();
}; };
const handleOk = () => {
setOpen(false);
onOk?.();
};
useLayoutEffect(() => {
onOpen?.(mergeOpen);
}, [mergeOpen]);
let layoutClassName = ""; let layoutClassName = "";
let panelClassName = ""; let panelClassName = "";
let titleClassName = ""; let titleClassName = "";
@ -74,11 +97,11 @@ const Modal = (props: ModalProps) => {
switch (type) { switch (type) {
case "bottom-drawer": case "bottom-drawer":
layoutClassName = layoutClassName = "fixed inset-0 flex flex-col w-[100%] bottom-0";
"fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%] md:"; panelClassName =
panelClassName = ""; "rounded-t-chat-model-select overflow-y-auto overflow-x-hidden";
titleClassName = ""; titleClassName = "px-4 py-3";
footerClassName = ""; footerClassName = "absolute w-[100%]";
break; break;
case "modal": case "modal":
default: default:
@ -107,9 +130,9 @@ const Modal = (props: ModalProps) => {
> >
<div className="flex-1">&nbsp;</div> <div className="flex-1">&nbsp;</div>
<div <div
className={`flex flex-col className={`flex flex-col flex-0
bg-moda-panel text-modal-mask bg-moda-panel text-modal-mask
${headerBordered ? " border-b border-modal-header-bottom" : ""} ${modelClassName}
${panelClassName} ${panelClassName}
`} `}
> >
@ -118,6 +141,11 @@ const Modal = (props: ModalProps) => {
className={` className={`
flex items-center justify-between gap-3 font-common flex items-center justify-between gap-3 font-common
md:text-chat-header-title md:font-bold md:leading-5 md:text-chat-header-title md:font-bold md:leading-5
${
headerBordered
? " border-b border-modal-header-bottom"
: ""
}
${titleClassName} ${titleClassName}
`} `}
> >
@ -136,7 +164,15 @@ const Modal = (props: ModalProps) => {
)} )}
</AlertDialog.Title> </AlertDialog.Title>
)} )}
{content} <div className="flex-1 overflow-hidden">
{typeof content === "function"
? content({
close: () => {
handleClose();
},
})
: content}
</div>
{!noFooter && ( {!noFooter && (
<div <div
className={` className={`
@ -155,10 +191,7 @@ const Modal = (props: ModalProps) => {
<AlertDialog.Action asChild> <AlertDialog.Action asChild>
<Btn <Btn
{...okBtnProps} {...okBtnProps}
onClick={() => { onClick={handleOk}
setOpen(false);
onOk?.();
}}
text={okText} text={okText}
className={`${btnCommonClass} ${okBtnClass}`} className={`${btnCommonClass} ${okBtnClass}`}
/> />
@ -166,7 +199,7 @@ const Modal = (props: ModalProps) => {
</div> </div>
)} )}
</div> </div>
<div className="flex-1">&nbsp;</div> {type === "modal" && <div className="flex-1">&nbsp;</div>}
</AlertDialog.Content> </AlertDialog.Content>
</AlertDialog.Portal> </AlertDialog.Portal>
</AlertDialog.Root> </AlertDialog.Root>
@ -252,8 +285,39 @@ Modal.warn = (props: Omit<WarnProps, "visible" | "onCancel" | "onOk">) => {
}); });
}; };
const Trigger = (props: ModalProps) => { export const Trigger = (props: TriggerProps) => {
return <></>; const { children, className, content, ...rest } = props;
const [internalVisible, setVisible] = useState(false);
return (
<>
<div
className={className}
onClick={() => {
setVisible(true);
}}
>
{children}
</div>
<Modal
{...rest}
visible={internalVisible}
onCancel={() => {
setVisible(false);
}}
content={
typeof content === "function"
? content({
close: () => {
setVisible(false);
},
})
: content
}
/>
</>
);
}; };
Modal.Trigger = Trigger; Modal.Trigger = Trigger;

View File

@ -7,6 +7,7 @@ import LogIcon from "@/app/icons/logIcon.svg";
import GobackIcon from "@/app/icons/goback.svg"; import GobackIcon from "@/app/icons/goback.svg";
import ShareIcon from "@/app/icons/shareIcon.svg"; import ShareIcon from "@/app/icons/shareIcon.svg";
import BottomArrow from "@/app/icons/bottomArrow.svg"; import BottomArrow from "@/app/icons/bottomArrow.svg";
import ModelSelect from "./ModelSelect";
export interface ChatHeaderProps { export interface ChatHeaderProps {
isMobileScreen: boolean; isMobileScreen: boolean;
@ -70,13 +71,7 @@ export default function ChatHeader(props: ChatHeaderProps) {
`} `}
> >
{isMobileScreen ? ( {isMobileScreen ? (
<div <ModelSelect />
className="flex items-center gap-1 cursor-pointer"
onClick={() => {}}
>
{currentModel}
<BottomArrow />
</div>
) : ( ) : (
Locale.Chat.SubTitle(session.messages.length) Locale.Chat.SubTitle(session.messages.length)
)} )}

View File

@ -3,14 +3,16 @@ import React, { useMemo, useRef } from "react";
import useRelativePosition, { import useRelativePosition, {
Orientation, Orientation,
} from "@/app/hooks/useRelativePosition"; } from "@/app/hooks/useRelativePosition";
import Locale from "@/app/locales";
import Selected from "@/app/icons/selectedIcon.svg";
import { useChatStore } from "@/app/store/chat"; import { useChatStore } from "@/app/store/chat";
import { useAllModels } from "@/app/utils/hooks"; import { useAllModels } from "@/app/utils/hooks";
import { ModelType, useAppConfig } from "@/app/store/config"; import { ModelType, useAppConfig } from "@/app/store/config";
import { showToast } from "@/app/components/ui-lib"; import { showToast } from "@/app/components/ui-lib";
import BottomArrow from "@/app/icons/downArrowLgIcon.svg"; import BottomArrow from "@/app/icons/downArrowLgIcon.svg";
import BottomArrowMobile from "@/app/icons/bottomArrow.svg"; import BottomArrowMobile from "@/app/icons/bottomArrow.svg";
import Modal, { TriggerProps } from "@/app/components/Modal";
import Selected from "@/app/icons/selectedIcon.svg";
const ModelSelect = () => { const ModelSelect = () => {
const config = useAppConfig(); const config = useAppConfig();
@ -60,15 +62,14 @@ const ModelSelect = () => {
}); });
}; };
const content = ( const content: TriggerProps["content"] = ({ close }) => (
<div <div className={`flex flex-col gap-1 overflow-x-hidden relative`}>
className={` flex flex-col gap-1 overflow-y-auto overflow-x-hidden relative h-[100%]`}
>
{models?.map((o) => ( {models?.map((o) => (
<div <div
key={o.displayName} key={o.displayName}
className={`flex items-center px-3 py-2 gap-3 rounded-action-btn hover:bg-select-option-hovered cursor-pointer`} className={`flex items-center px-3 py-2 gap-3 rounded-action-btn hover:bg-select-option-hovered cursor-pointer`}
onClick={() => { onClick={() => {
close();
chatStore.updateCurrentSession((session) => { chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.model = o.name as ModelType; session.mask.modelConfig.model = o.name as ModelType;
session.mask.syncGlobalConfig = false; session.mask.syncGlobalConfig = false;
@ -90,39 +91,41 @@ const ModelSelect = () => {
if (isMobileScreen) { if (isMobileScreen) {
return ( return (
<Popover <Modal.Trigger
content={content} content={(e) => (
trigger="click" <div className="h-[100%] overflow-y-auto" ref={contentRef}>
noArrow {content(e)}
placement={ </div>
position?.poi.relativePosition[1] !== Orientation.bottom ? "lb" : "lt" )}
} type="bottom-drawer"
popoverClassName="border border-select-popover rounded-lg shadow-select-popover-shadow w-actions-popover bg-select-popover-panel max-h-chat-actions-select-model-popover" onOpen={(e) => {
onShow={(e) => {
if (e) { if (e) {
autoScrollToSelectedModal(); autoScrollToSelectedModal();
getRelativePosition(rootRef.current!, ""); getRelativePosition(rootRef.current!, "");
} }
}} }}
getPopoverPanelRef={(ref) => (contentRef.current = ref.current)} title={Locale.Chat.SelectModel}
headerBordered
noFooter
modelClassName="h-model-bottom-drawer"
> >
<div className="flex items-center gap-1 cursor-pointer" ref={rootRef}> <div className="flex items-center gap-1 cursor-pointer" ref={rootRef}>
{currentModel} {currentModel}
<BottomArrowMobile /> <BottomArrowMobile />
</div> </div>
</Popover> </Modal.Trigger>
); );
} }
return ( return (
<Popover <Popover
content={content} content={content({ close: () => {} })}
trigger="click" trigger="click"
noArrow noArrow
placement={ placement={
position?.poi.relativePosition[1] !== Orientation.bottom ? "lb" : "lt" position?.poi.relativePosition[1] !== Orientation.bottom ? "lb" : "lt"
} }
popoverClassName="border border-select-popover rounded-lg shadow-select-popover-shadow w-actions-popover bg-select-popover-panel max-h-chat-actions-select-model-popover" popoverClassName="border border-select-popover rounded-lg shadow-select-popover-shadow w-actions-popover bg-select-popover-panel max-h-chat-actions-select-model-popover !py-0"
onShow={(e) => { onShow={(e) => {
if (e) { if (e) {
autoScrollToSelectedModal(); autoScrollToSelectedModal();
@ -135,7 +138,7 @@ const ModelSelect = () => {
className="flex items-center justify-center gap-1 cursor-pointer rounded-chat-model-select pl-3 pr-2.5 py-2 font-common leading-4 bg-chat-actions-select-model hover:bg-chat-actions-select-model-hover" className="flex items-center justify-center gap-1 cursor-pointer rounded-chat-model-select pl-3 pr-2.5 py-2 font-common leading-4 bg-chat-actions-select-model hover:bg-chat-actions-select-model-hover"
ref={rootRef} ref={rootRef}
> >
<div className="line-clamp-1 max-w-chat-actions-select-model"> <div className="line-clamp-1 max-w-chat-actions-select-model text-sm-title">
{currentModel} {currentModel}
</div> </div>
<BottomArrow /> <BottomArrow />

View File

@ -0,0 +1,124 @@
import { Draggable } from "@hello-pangea/dnd";
import Locale from "@/app/locales";
import { useLocation } from "react-router-dom";
import { Path } from "@/app/constant";
import { Mask } from "@/app/store/mask";
import { useRef, useEffect } from "react";
import DeleteChatIcon from "@/app/icons/deleteChatIcon.svg";
import { getTime } from "@/app/utils";
import DeleteIcon from "@/app/icons/deleteIcon.svg";
import LogIcon from "@/app/icons/logIcon.svg";
import HoverPopover from "@/app/components/HoverPopover";
export default function SessionItem(props: {
onClick?: () => void;
onDelete?: () => void;
title: string;
count: number;
time: string;
selected: boolean;
id: string;
index: number;
narrow?: boolean;
mask: Mask;
isMobileScreen: boolean;
}) {
const draggableRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (props.selected && draggableRef.current) {
draggableRef.current?.scrollIntoView({
block: "center",
});
}
}, [props.selected]);
const { pathname: currentPath } = useLocation();
return (
<Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => (
<div
className={`
group/chat-menu-list relative flex p-3 items-center gap-2 self-stretch rounded-md mb-2
border
bg-chat-menu-session-unselected border-chat-menu-session-unselected cursor-pointer
${
props.selected &&
(currentPath === Path.Chat || currentPath === Path.Home)
? `!bg-chat-menu-session-selected !border-chat-menu-session-selected`
: `hover:bg-chat-menu-session-hovered hover:chat-menu-session-hovered`
}
`}
onClick={props.onClick}
// ref={(ele) => {
// draggableRef.current = ele;
// provided.innerRef(ele);
// }}
// {...provided.draggableProps}
// {...provided.dragHandleProps}
title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
props.count,
)}`}
>
<div className=" flex-shrink-0">
<LogIcon />
</div>
<div className="flex flex-col flex-1">
<div className={`flex justify-between items-center`}>
<div
className={` text-text-chat-menu-item-title text-sm-title line-clamp-1 flex-1`}
>
{props.title}
</div>
<div
className={`text-text-chat-menu-item-time text-sm group-hover/chat-menu-list:opacity-0 pl-3`}
>
{getTime(props.time)}
</div>
</div>
<div className={`text-text-chat-menu-item-description text-sm`}>
{Locale.ChatItem.ChatItemCount(props.count)}
</div>
</div>
<HoverPopover
content={
<div
className={`flex items-center gap-3 p-3 rounded-action-btn leading-6 cursor-pointer`}
onClickCapture={(e) => {
props.onDelete?.();
e.preventDefault();
e.stopPropagation();
}}
>
<DeleteChatIcon />
<div className="flex-1 font-common text-actions-popover-menu-item">
{Locale.Chat.Actions.Delete}
</div>
</div>
}
popoverClassName={`
px-2 py-1 border-delete-chat-popover bg-delete-chat-popover-panel rounded-md shadow-delete-chat-popover-shadow
`}
noArrow
align={props.isMobileScreen ? "end" : "start"}
>
<div
className={`
!absolute top-[50%] translate-y-[-50%] right-3 pointer-events-none opacity-0
group-hover/chat-menu-list:pointer-events-auto
group-hover/chat-menu-list:opacity-100
hover:bg-select-hover rounded-chat-img
`}
>
<DeleteIcon />
</div>
</HoverPopover>
</div>
)}
</Draggable>
);
}

View File

@ -1,7 +1,6 @@
import { import {
DragDropContext, DragDropContext,
Droppable, Droppable,
Draggable,
OnDragEndResponder, OnDragEndResponder,
} from "@hello-pangea/dnd"; } from "@hello-pangea/dnd";
@ -10,130 +9,15 @@ import { useAppConfig, useChatStore } from "@/app/store";
import Locale from "@/app/locales"; import Locale from "@/app/locales";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { Path } from "@/app/constant"; import { Path } from "@/app/constant";
import { Mask } from "@/app/store/mask"; import { useEffect } from "react";
import { useRef, useEffect } from "react";
import AddIcon from "@/app/icons/addIcon.svg"; import AddIcon from "@/app/icons/addIcon.svg";
import NextChatTitle from "@/app/icons/nextchatTitle.svg"; import NextChatTitle from "@/app/icons/nextchatTitle.svg";
import DeleteChatIcon from "@/app/icons/deleteChatIcon.svg";
import { getTime } from "@/app/utils";
import DeleteIcon from "@/app/icons/deleteIcon.svg";
import LogIcon from "@/app/icons/logIcon.svg";
import MenuLayout from "@/app/components/MenuLayout"; import MenuLayout from "@/app/components/MenuLayout";
import Panel from "./ChatPanel"; import Panel from "./ChatPanel";
import Modal from "@/app/components/Modal"; import Modal from "@/app/components/Modal";
import HoverPopover from "@/app/components/HoverPopover"; import SessionItem from "./components/SessionItem";
export function SessionItem(props: {
onClick?: () => void;
onDelete?: () => void;
title: string;
count: number;
time: string;
selected: boolean;
id: string;
index: number;
narrow?: boolean;
mask: Mask;
isMobileScreen: boolean;
}) {
const draggableRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (props.selected && draggableRef.current) {
draggableRef.current?.scrollIntoView({
block: "center",
});
}
}, [props.selected]);
const { pathname: currentPath } = useLocation();
return (
<Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => (
<div
className={`
group/chat-menu-list relative flex p-3 items-center gap-2 self-stretch rounded-md mb-2
border
bg-chat-menu-session-unselected border-chat-menu-session-unselected
${
props.selected &&
(currentPath === Path.Chat || currentPath === Path.Home)
? `!bg-chat-menu-session-selected !border-chat-menu-session-selected`
: `hover:bg-chat-menu-session-hovered hover:chat-menu-session-hovered`
}
`}
onClick={props.onClick}
ref={(ele) => {
draggableRef.current = ele;
provided.innerRef(ele);
}}
{...provided.draggableProps}
{...provided.dragHandleProps}
title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
props.count,
)}`}
>
<div className=" flex-shrink-0">
<LogIcon />
</div>
<div className="flex flex-col flex-1">
<div className={`flex justify-between items-center`}>
<div
className={` text-text-chat-menu-item-title text-sm-title line-clamp-1 flex-1`}
>
{props.title}
</div>
<div
className={`text-text-chat-menu-item-time text-sm group-hover/chat-menu-list:opacity-0 pl-3`}
>
{getTime(props.time)}
</div>
</div>
<div className={`text-text-chat-menu-item-description text-sm`}>
{Locale.ChatItem.ChatItemCount(props.count)}
</div>
</div>
<HoverPopover
content={
<div
className={`flex items-center gap-3 p-3 rounded-action-btn leading-6 cursor-pointer`}
onClickCapture={(e) => {
props.onDelete?.();
e.preventDefault();
e.stopPropagation();
}}
>
<DeleteChatIcon />
<div className="flex-1 font-common text-actions-popover-menu-item">
{Locale.Chat.Actions.Delete}
</div>
</div>
}
popoverClassName={`
px-2 py-1 border-delete-chat-popover bg-delete-chat-popover-panel rounded-md shadow-delete-chat-popover-shadow
`}
noArrow
align={props.isMobileScreen ? "end" : "start"}
>
<div
className={`
!absolute top-[50%] translate-y-[-50%] right-3 pointer-events-none opacity-0
group-hover/chat-menu-list:pointer-events-auto
group-hover/chat-menu-list:opacity-100
hover:bg-select-hover rounded-chat-img
`}
>
<DeleteIcon />
</div>
</HoverPopover>
</div>
)}
</Draggable>
);
}
export default MenuLayout(function SessionList(props) { export default MenuLayout(function SessionList(props) {
const { setShowPanel } = props; const { setShowPanel } = props;
@ -225,8 +109,8 @@ export default MenuLayout(function SessionList(props) {
<Droppable droppableId="chat-list"> <Droppable droppableId="chat-list">
{(provided) => ( {(provided) => (
<div <div
ref={provided.innerRef} // ref={provided.innerRef}
{...provided.droppableProps} // {...provided.droppableProps}
className={`w-[100%]`} className={`w-[100%]`}
> >
{sessions.map((item, i) => ( {sessions.map((item, i) => (

View File

@ -84,6 +84,7 @@ const cn = {
SaveAs: "存为面具", SaveAs: "存为面具",
}, },
IsContext: "预设提示词", IsContext: "预设提示词",
SelectModel: "选择模型",
}, },
Export: { Export: {
Title: "分享聊天记录", Title: "分享聊天记录",

View File

@ -87,6 +87,7 @@ const en: LocaleType = {
SaveAs: "Save as Mask", SaveAs: "Save as Mask",
}, },
IsContext: "Contextual Prompt", IsContext: "Contextual Prompt",
SelectModel: "Choose model",
}, },
Export: { Export: {
Title: "Export Messages", Title: "Export Messages",

View File

@ -30,6 +30,10 @@ body {
outline: none; outline: none;
} }
* {
font-weight: 400;
}
.light-new { .light-new {
--global-bg: #e3e3ed; --global-bg: #e3e3ed;
--actions-bar-btn-default-bg: #2e42f3; --actions-bar-btn-default-bg: #2e42f3;

View File

@ -54,6 +54,7 @@ module.exports = {
'slide-btn': '18px', 'slide-btn': '18px',
'switch': '1rem', 'switch': '1rem',
'chat-header-title-mobile': '19px', 'chat-header-title-mobile': '19px',
'model-bottom-drawer': 'calc(100vh - 110px)',
}, },
minWidth: { minWidth: {
'select-mobile-lg': '200px', 'select-mobile-lg': '200px',