chat-list.tsx 4.9 KB

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