123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- import { useEffect, useMemo, useState } from "react";
- import { ChatMessage, useAppConfig, useChatStore } from "../store";
- import { Updater } from "../typing";
- import { IconButton } from "./button";
- import { Avatar } from "./emoji";
- import { MaskAvatar } from "./mask";
- import Locale from "../locales";
- import styles from "./message-selector.module.scss";
- function useShiftRange() {
- const [startIndex, setStartIndex] = useState<number>();
- const [endIndex, setEndIndex] = useState<number>();
- const [shiftDown, setShiftDown] = useState(false);
- const onClickIndex = (index: number) => {
- if (shiftDown && startIndex !== undefined) {
- setEndIndex(index);
- } else {
- setStartIndex(index);
- setEndIndex(undefined);
- }
- };
- useEffect(() => {
- const onKeyDown = (e: KeyboardEvent) => {
- if (e.key !== "Shift") return;
- setShiftDown(true);
- };
- const onKeyUp = (e: KeyboardEvent) => {
- if (e.key !== "Shift") return;
- setShiftDown(false);
- setStartIndex(undefined);
- setEndIndex(undefined);
- };
- window.addEventListener("keyup", onKeyUp);
- window.addEventListener("keydown", onKeyDown);
- return () => {
- window.removeEventListener("keyup", onKeyUp);
- window.removeEventListener("keydown", onKeyDown);
- };
- }, []);
- return {
- onClickIndex,
- startIndex,
- endIndex,
- };
- }
- export function useMessageSelector() {
- const [selection, setSelection] = useState(new Set<string>());
- const updateSelection: Updater<Set<string>> = (updater) => {
- const newSelection = new Set<string>(selection);
- updater(newSelection);
- setSelection(newSelection);
- };
- return {
- selection,
- updateSelection,
- };
- }
- export function MessageSelector(props: {
- selection: Set<string>;
- updateSelection: Updater<Set<string>>;
- defaultSelectAll?: boolean;
- onSelected?: (messages: ChatMessage[]) => void;
- }) {
- const chatStore = useChatStore();
- const session = chatStore.currentSession();
- const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
- const allMessages = useMemo(() => {
- let startIndex = Math.max(0, session.clearContextIndex ?? 0);
- if (startIndex === session.messages.length - 1) {
- startIndex = 0;
- }
- return session.messages.slice(startIndex);
- }, [session.messages, session.clearContextIndex]);
- const messages = useMemo(
- () =>
- allMessages.filter(
- (m, i) =>
- m.id && // message must have id
- isValid(m) &&
- (i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
- ),
- [allMessages],
- );
- const messageCount = messages.length;
- const config = useAppConfig();
- const [searchInput, setSearchInput] = useState("");
- const [searchIds, setSearchIds] = useState(new Set<string>());
- const isInSearchResult = (id: string) => {
- return searchInput.length === 0 || searchIds.has(id);
- };
- const doSearch = (text: string) => {
- const searchResults = new Set<string>();
- if (text.length > 0) {
- messages.forEach((m) =>
- m.content.includes(text) ? searchResults.add(m.id!) : null,
- );
- }
- setSearchIds(searchResults);
- };
- // for range selection
- const { startIndex, endIndex, onClickIndex } = useShiftRange();
- const selectAll = () => {
- props.updateSelection((selection) =>
- messages.forEach((m) => selection.add(m.id!)),
- );
- };
- useEffect(() => {
- if (props.defaultSelectAll) {
- selectAll();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- useEffect(() => {
- if (startIndex === undefined || endIndex === undefined) {
- return;
- }
- const [start, end] = [startIndex, endIndex].sort((a, b) => a - b);
- props.updateSelection((selection) => {
- for (let i = start; i <= end; i += 1) {
- selection.add(messages[i].id ?? i);
- }
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [startIndex, endIndex]);
- const LATEST_COUNT = 4;
- return (
- <div className={styles["message-selector"]}>
- <div className={styles["message-filter"]}>
- <input
- type="text"
- placeholder={Locale.Select.Search}
- className={styles["filter-item"] + " " + styles["search-bar"]}
- value={searchInput}
- onInput={(e) => {
- setSearchInput(e.currentTarget.value);
- doSearch(e.currentTarget.value);
- }}
- ></input>
- <div className={styles["actions"]}>
- <IconButton
- text={Locale.Select.All}
- bordered
- className={styles["filter-item"]}
- onClick={selectAll}
- />
- <IconButton
- text={Locale.Select.Latest}
- bordered
- className={styles["filter-item"]}
- onClick={() =>
- props.updateSelection((selection) => {
- selection.clear();
- messages
- .slice(messageCount - LATEST_COUNT)
- .forEach((m) => selection.add(m.id!));
- })
- }
- />
- <IconButton
- text={Locale.Select.Clear}
- bordered
- className={styles["filter-item"]}
- onClick={() =>
- props.updateSelection((selection) => selection.clear())
- }
- />
- </div>
- </div>
- <div className={styles["messages"]}>
- {messages.map((m, i) => {
- if (!isInSearchResult(m.id!)) return null;
- const id = m.id ?? i;
- const isSelected = props.selection.has(id);
- return (
- <div
- className={`${styles["message"]} ${
- props.selection.has(m.id!) && styles["message-selected"]
- }`}
- key={i}
- onClick={() => {
- props.updateSelection((selection) => {
- selection.has(id) ? selection.delete(id) : selection.add(id);
- });
- onClickIndex(i);
- }}
- >
- <div className={styles["avatar"]}>
- {m.role === "user" ? (
- <Avatar avatar={config.avatar}></Avatar>
- ) : (
- <MaskAvatar
- avatar={session.mask.avatar}
- model={m.model || session.mask.modelConfig.model}
- />
- )}
- </div>
- <div className={styles["body"]}>
- <div className={styles["date"]}>
- {new Date(m.date).toLocaleString()}
- </div>
- <div className={`${styles["content"]} one-line`}>
- {m.content}
- </div>
- </div>
- <div className={styles["checkbox"]}>
- <input type="checkbox" checked={isSelected}></input>
- </div>
- </div>
- );
- })}
- </div>
- </div>
- );
- }
|