chat-list.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import DeleteIcon from "../icons/delete.svg";
  2. import styles from "./home.module.scss";
  3. import {
  4. DragDropContext,
  5. Droppable,
  6. Draggable,
  7. OnDragEndResponder,
  8. } from "@hello-pangea/dnd";
  9. import { useChatStore } from "../store";
  10. import Locale from "../locales";
  11. import { useLocation, useNavigate } from "react-router-dom";
  12. import { Path } from "../constant";
  13. import { MaskAvatar } from "./mask";
  14. import { Mask } from "../store/mask";
  15. import { useRef, useEffect } from "react";
  16. import { showConfirm } from "./ui-lib";
  17. import { useMobileScreen } from "../utils";
  18. export function ChatItem(props: {
  19. onClick?: () => void;
  20. onDelete?: () => void;
  21. title: string;
  22. count: number;
  23. time: string;
  24. selected: boolean;
  25. id: string;
  26. index: number;
  27. narrow?: boolean;
  28. mask: Mask;
  29. }) {
  30. const draggableRef = useRef<HTMLDivElement | null>(null);
  31. useEffect(() => {
  32. if (props.selected && draggableRef.current) {
  33. draggableRef.current?.scrollIntoView({
  34. block: "center",
  35. });
  36. }
  37. }, [props.selected]);
  38. const { pathname: currentPath } = useLocation();
  39. return (
  40. <Draggable draggableId={`${props.id}`} index={props.index}>
  41. {(provided) => (
  42. <div
  43. className={`${styles["chat-item"]} ${
  44. props.selected &&
  45. (currentPath === Path.Chat || currentPath === Path.Home) &&
  46. styles["chat-item-selected"]
  47. }`}
  48. onClick={props.onClick}
  49. ref={(ele) => {
  50. draggableRef.current = ele;
  51. provided.innerRef(ele);
  52. }}
  53. {...provided.draggableProps}
  54. {...provided.dragHandleProps}
  55. title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
  56. props.count,
  57. )}`}
  58. >
  59. {props.narrow ? (
  60. <div className={styles["chat-item-narrow"]}>
  61. <div className={styles["chat-item-avatar"] + " no-dark"}>
  62. <MaskAvatar
  63. avatar={props.mask.avatar}
  64. model={props.mask.modelConfig.model}
  65. />
  66. </div>
  67. <div className={styles["chat-item-narrow-count"]}>
  68. {props.count}
  69. </div>
  70. </div>
  71. ) : (
  72. <>
  73. <div className={styles["chat-item-title"]}>{props.title}</div>
  74. <div className={styles["chat-item-info"]}>
  75. <div className={styles["chat-item-count"]}>
  76. {Locale.ChatItem.ChatItemCount(props.count)}
  77. </div>
  78. <div className={styles["chat-item-date"]}>{props.time}</div>
  79. </div>
  80. </>
  81. )}
  82. <div
  83. className={styles["chat-item-delete"]}
  84. onClickCapture={(e) => {
  85. props.onDelete?.();
  86. e.preventDefault();
  87. e.stopPropagation();
  88. }}
  89. >
  90. <DeleteIcon />
  91. </div>
  92. </div>
  93. )}
  94. </Draggable>
  95. );
  96. }
  97. export function ChatList(props: { narrow?: boolean }) {
  98. const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
  99. (state) => [
  100. state.sessions,
  101. state.currentSessionIndex,
  102. state.selectSession,
  103. state.moveSession,
  104. ],
  105. );
  106. const chatStore = useChatStore();
  107. const navigate = useNavigate();
  108. const isMobileScreen = useMobileScreen();
  109. const onDragEnd: OnDragEndResponder = (result) => {
  110. const { destination, source } = result;
  111. if (!destination) {
  112. return;
  113. }
  114. if (
  115. destination.droppableId === source.droppableId &&
  116. destination.index === source.index
  117. ) {
  118. return;
  119. }
  120. moveSession(source.index, destination.index);
  121. };
  122. return (
  123. <DragDropContext onDragEnd={onDragEnd}>
  124. <Droppable droppableId="chat-list">
  125. {(provided) => (
  126. <div
  127. className={styles["chat-list"]}
  128. ref={provided.innerRef}
  129. {...provided.droppableProps}
  130. >
  131. {sessions.map((item, i) => (
  132. <ChatItem
  133. title={item.topic}
  134. time={new Date(item.lastUpdate).toLocaleString()}
  135. count={item.messages.length}
  136. key={item.id}
  137. id={item.id}
  138. index={i}
  139. selected={i === selectedIndex}
  140. onClick={() => {
  141. navigate(Path.Chat);
  142. selectSession(i);
  143. }}
  144. onDelete={async () => {
  145. if (
  146. (!props.narrow && !isMobileScreen) ||
  147. (await showConfirm(Locale.Home.DeleteChat))
  148. ) {
  149. chatStore.deleteSession(i);
  150. }
  151. }}
  152. narrow={props.narrow}
  153. mask={item.mask}
  154. />
  155. ))}
  156. {provided.placeholder}
  157. </div>
  158. )}
  159. </Droppable>
  160. </DragDropContext>
  161. );
  162. }