ChatGPT-Next-Web/app/components/chat-list.tsx
2024-09-26 13:48:53 +08:00

221 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import DeleteIcon from "../icons/delete.svg";
import styles from "./home.module.scss";
import {
DragDropContext,
Droppable,
Draggable,
OnDragEndResponder,
DraggableProvided,
} 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";
// motion
import QueueAnim from "rc-queue-anim";
export function ChatItem(props: {
onClick?: () => void;
onDelete?: () => void;
title: string;
count: number;
time: string;
selected: boolean;
id: string;
index: number;
narrow?: boolean;
mask: Mask;
provided: DraggableProvided;
}) {
const draggableRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (props.selected && draggableRef.current) {
draggableRef.current?.scrollIntoView({
block: "center",
});
}
}, [props.selected]);
const { pathname: currentPath } = useLocation();
return (
<div
className={`${styles["chat-item"]} ${
props.selected &&
(currentPath === Path.Chat || currentPath === Path.Home) &&
styles["chat-item-selected"]
}`}
onClick={props.onClick}
ref={(ele) => {
draggableRef.current = ele;
props.provided.innerRef(ele);
}}
{...props.provided.draggableProps}
{...props.provided.dragHandleProps}
title={`${props.title}\n${Locale.ChatItem.ChatItemCount(props.count)}`}
>
{props.narrow ? (
<div className={styles["chat-item-narrow"]}>
<div className={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>
);
}
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"
renderClone={(provided, snapshot, rubic) => (
<ChatItem
title={sessions[rubic.source.index].topic}
time={new Date(
sessions[rubic.source.index].lastUpdate,
).toLocaleString()}
count={sessions[rubic.source.index].messages.length}
key={sessions[rubic.source.index].id}
id={sessions[rubic.source.index].id}
index={rubic.source.index}
selected={rubic.source.index === selectedIndex}
onClick={() => {
navigate(Path.Chat);
selectSession(rubic.source.index);
}}
onDelete={async () => {
if (
(!props.narrow && !isMobileScreen) ||
(await showConfirm(Locale.Home.DeleteChat))
) {
chatStore.deleteSession(rubic.source.index);
}
}}
narrow={props.narrow}
mask={sessions[rubic.source.index].mask}
provided={provided}
/>
)}
>
{(provided, snapshot) => (
<div
className={styles["chat-list"]}
ref={provided.innerRef}
{...provided.droppableProps}
>
<QueueAnim
delay={[100, 0]}
ease={"easeOutQuart"} // "easeInOutQuart"
duration={[550, 450]}
animConfig={[
{ opacity: [1, 0], translateY: [0, -30], height: [71, 0] },
{ height: 0 },
]}
// TODO修复添加元素其他元素平移动画问题但下面的好像不生效全靠指定高度
onEnd={({ key, type, target }) => {
if (type === "enter") target.style.height = "auto";
}}
// interval={150}
>
{sessions.map((item, i) => (
<div key={item.id}>
<Draggable draggableId={`${item.id}`} index={i}>
{(provided, snapshot) => (
<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={provided}
/>
)}
</Draggable>
</div>
))}
</QueueAnim>
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
}