You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			175 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			TypeScript
		
	
		
		
			
		
	
	
			175 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			TypeScript
		
	
| 
											9 months ago
										 | import DeleteIcon from "../icons/delete.svg"; | ||
|  | 
 | ||
|  | import styles from "./home.module.scss"; | ||
|  | import { | ||
|  |   DragDropContext, | ||
|  |   Droppable, | ||
|  |   Draggable, | ||
|  |   OnDragEndResponder, | ||
|  | } from "@hello-pangea/dnd"; | ||
|  | 
 | ||
|  | import { useChatStore } from "../store"; | ||
|  | 
 | ||
|  | import Locale from "../locales"; | ||
|  | import { useLocation, useNavigate } from "react-router-dom"; | ||
|  | import { Path } from "../constant"; | ||
|  | import { MaskAvatar } from "./mask"; | ||
|  | import { Mask } from "../store/mask"; | ||
|  | import { useRef, useEffect } from "react"; | ||
|  | import { showConfirm } from "./ui-lib"; | ||
|  | import { useMobileScreen } from "../utils"; | ||
|  | import clsx from "clsx"; | ||
|  | 
 | ||
|  | export function ChatItem(props: { | ||
|  |   onClick?: () => void; | ||
|  |   onDelete?: () => void; | ||
|  |   title: string; | ||
|  |   count: number; | ||
|  |   time: string; | ||
|  |   selected: boolean; | ||
|  |   id: string; | ||
|  |   index: number; | ||
|  |   narrow?: boolean; | ||
|  |   mask: Mask; | ||
|  | }) { | ||
|  |   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={clsx(styles["chat-item"], { | ||
|  |             [styles["chat-item-selected"]]: | ||
|  |               props.selected && | ||
|  |               (currentPath === Path.Chat || currentPath === Path.Home), | ||
|  |           })} | ||
|  |           onClick={props.onClick} | ||
|  |           ref={(ele) => { | ||
|  |             draggableRef.current = ele; | ||
|  |             provided.innerRef(ele); | ||
|  |           }} | ||
|  |           {...provided.draggableProps} | ||
|  |           {...provided.dragHandleProps} | ||
|  |           title={`${props.title}\n${Locale.ChatItem.ChatItemCount( | ||
|  |             props.count, | ||
|  |           )}`}
 | ||
|  |         > | ||
|  |           {props.narrow ? ( | ||
|  |             <div className={styles["chat-item-narrow"]}> | ||
|  |               <div className={clsx(styles["chat-item-avatar"], "no-dark")}> | ||
|  |                 <MaskAvatar | ||
|  |                   avatar={props.mask.avatar} | ||
|  |                   model={props.mask.modelConfig.model} | ||
|  |                 /> | ||
|  |               </div> | ||
|  |               <div className={styles["chat-item-narrow-count"]}> | ||
|  |                 {props.count} | ||
|  |               </div> | ||
|  |             </div> | ||
|  |           ) : ( | ||
|  |             <> | ||
|  |               <div className={styles["chat-item-title"]}>{props.title}</div> | ||
|  |               <div className={styles["chat-item-info"]}> | ||
|  |                 <div className={styles["chat-item-count"]}> | ||
|  |                   {Locale.ChatItem.ChatItemCount(props.count)} | ||
|  |                 </div> | ||
|  |                 <div className={styles["chat-item-date"]}>{props.time}</div> | ||
|  |               </div> | ||
|  |             </> | ||
|  |           )} | ||
|  | 
 | ||
|  |           <div | ||
|  |             className={styles["chat-item-delete"]} | ||
|  |             onClickCapture={(e) => { | ||
|  |               props.onDelete?.(); | ||
|  |               e.preventDefault(); | ||
|  |               e.stopPropagation(); | ||
|  |             }} | ||
|  |           > | ||
|  |             <DeleteIcon /> | ||
|  |           </div> | ||
|  |         </div> | ||
|  |       )} | ||
|  |     </Draggable> | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function ChatList(props: { narrow?: boolean }) { | ||
|  |   const [sessions, selectedIndex, selectSession, moveSession] = useChatStore( | ||
|  |     (state) => [ | ||
|  |       state.sessions, | ||
|  |       state.currentSessionIndex, | ||
|  |       state.selectSession, | ||
|  |       state.moveSession, | ||
|  |     ], | ||
|  |   ); | ||
|  |   const chatStore = useChatStore(); | ||
|  |   const navigate = useNavigate(); | ||
|  |   const isMobileScreen = useMobileScreen(); | ||
|  | 
 | ||
|  |   const onDragEnd: OnDragEndResponder = (result) => { | ||
|  |     const { destination, source } = result; | ||
|  |     if (!destination) { | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     if ( | ||
|  |       destination.droppableId === source.droppableId && | ||
|  |       destination.index === source.index | ||
|  |     ) { | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     moveSession(source.index, destination.index); | ||
|  |   }; | ||
|  | 
 | ||
|  |   return ( | ||
|  |     <DragDropContext onDragEnd={onDragEnd}> | ||
|  |       <Droppable droppableId="chat-list"> | ||
|  |         {(provided) => ( | ||
|  |           <div | ||
|  |             className={styles["chat-list"]} | ||
|  |             ref={provided.innerRef} | ||
|  |             {...provided.droppableProps} | ||
|  |           > | ||
|  |             {sessions.map((item, i) => ( | ||
|  |               <ChatItem | ||
|  |                 title={item.topic} | ||
|  |                 time={new Date(item.lastUpdate).toLocaleString()} | ||
|  |                 count={item.messages.length} | ||
|  |                 key={item.id} | ||
|  |                 id={item.id} | ||
|  |                 index={i} | ||
|  |                 selected={i === selectedIndex} | ||
|  |                 onClick={() => { | ||
|  |                   navigate(Path.Chat); | ||
|  |                   selectSession(i); | ||
|  |                 }} | ||
|  |                 onDelete={async () => { | ||
|  |                   if ( | ||
|  |                     (!props.narrow && !isMobileScreen) || | ||
|  |                     (await showConfirm(Locale.Home.DeleteChat)) | ||
|  |                   ) { | ||
|  |                     chatStore.deleteSession(i); | ||
|  |                   } | ||
|  |                 }} | ||
|  |                 narrow={props.narrow} | ||
|  |                 mask={item.mask} | ||
|  |               /> | ||
|  |             ))} | ||
|  |             {provided.placeholder} | ||
|  |           </div> | ||
|  |         )} | ||
|  |       </Droppable> | ||
|  |     </DragDropContext> | ||
|  |   ); | ||
|  | } |