search-chat.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { useState, useEffect, useRef } from "react";
  2. import { ErrorBoundary } from "./error";
  3. import styles from "./mask.module.scss";
  4. import { useNavigate } from "react-router-dom";
  5. import { IconButton } from "./button";
  6. import CloseIcon from "../icons/close.svg";
  7. import EyeIcon from "../icons/eye.svg";
  8. import Locale from "../locales";
  9. import { Path } from "../constant";
  10. import { useChatStore } from "../store";
  11. type Item = {
  12. id: number;
  13. name: string;
  14. content: string;
  15. };
  16. export function SearchChatPage() {
  17. const navigate = useNavigate();
  18. const chatStore = useChatStore();
  19. const sessions = chatStore.sessions;
  20. const selectSession = chatStore.selectSession;
  21. const [searchResults, setSearchResults] = useState<Item[]>([]);
  22. const previousValueRef = useRef<string>("");
  23. const searchInputRef = useRef<HTMLInputElement>(null);
  24. const doSearch = (text: string) => {
  25. const lowerCaseText = text.toLowerCase();
  26. const results: Item[] = [];
  27. sessions.forEach((session, index) => {
  28. const fullTextContents: string[] = [];
  29. session.messages.forEach((message) => {
  30. const content = message.content as string;
  31. if (!content.toLowerCase || content === "") return;
  32. const lowerCaseContent = content.toLowerCase();
  33. // full text search
  34. let pos = lowerCaseContent.indexOf(lowerCaseText);
  35. while (pos !== -1) {
  36. const start = Math.max(0, pos - 35);
  37. const end = Math.min(content.length, pos + lowerCaseText.length + 35);
  38. fullTextContents.push(content.substring(start, end));
  39. pos = lowerCaseContent.indexOf(
  40. lowerCaseText,
  41. pos + lowerCaseText.length,
  42. );
  43. }
  44. });
  45. if (fullTextContents.length > 0) {
  46. results.push({
  47. id: index,
  48. name: session.topic,
  49. content: fullTextContents.join("... "), // concat content with...
  50. });
  51. }
  52. });
  53. // sort by length of matching content
  54. results.sort((a, b) => b.content.length - a.content.length);
  55. return results;
  56. };
  57. useEffect(() => {
  58. const intervalId = setInterval(() => {
  59. if (searchInputRef.current) {
  60. const currentValue = searchInputRef.current.value;
  61. if (currentValue !== previousValueRef.current) {
  62. if (currentValue.length > 0) {
  63. const result = doSearch(currentValue);
  64. setSearchResults(result);
  65. }
  66. previousValueRef.current = currentValue;
  67. }
  68. }
  69. }, 1000);
  70. // Cleanup the interval on component unmount
  71. return () => clearInterval(intervalId);
  72. }, [doSearch]);
  73. return (
  74. <ErrorBoundary>
  75. <div className={styles["mask-page"]}>
  76. {/* header */}
  77. <div className="window-header">
  78. <div className="window-header-title">
  79. <div className="window-header-main-title">
  80. {Locale.SearchChat.Page.Title}
  81. </div>
  82. <div className="window-header-submai-title">
  83. {Locale.SearchChat.Page.SubTitle(searchResults.length)}
  84. </div>
  85. </div>
  86. <div className="window-actions">
  87. <div className="window-action-button">
  88. <IconButton
  89. icon={<CloseIcon />}
  90. bordered
  91. onClick={() => navigate(-1)}
  92. />
  93. </div>
  94. </div>
  95. </div>
  96. <div className={styles["mask-page-body"]}>
  97. <div className={styles["mask-filter"]}>
  98. {/**搜索输入框 */}
  99. <input
  100. type="text"
  101. className={styles["search-bar"]}
  102. placeholder={Locale.SearchChat.Page.Search}
  103. autoFocus
  104. ref={searchInputRef}
  105. onKeyDown={(e) => {
  106. if (e.key === "Enter") {
  107. e.preventDefault();
  108. const searchText = e.currentTarget.value;
  109. if (searchText.length > 0) {
  110. const result = doSearch(searchText);
  111. setSearchResults(result);
  112. }
  113. }
  114. }}
  115. />
  116. </div>
  117. <div>
  118. {searchResults.map((item) => (
  119. <div
  120. className={styles["mask-item"]}
  121. key={item.id}
  122. onClick={() => {
  123. navigate(Path.Chat);
  124. selectSession(item.id);
  125. }}
  126. style={{ cursor: "pointer" }}
  127. >
  128. {/** 搜索匹配的文本 */}
  129. <div className={styles["mask-header"]}>
  130. <div className={styles["mask-title"]}>
  131. <div className={styles["mask-name"]}>{item.name}</div>
  132. {item.content.slice(0, 70)}
  133. </div>
  134. </div>
  135. {/** 操作按钮 */}
  136. <div className={styles["mask-actions"]}>
  137. <IconButton
  138. icon={<EyeIcon />}
  139. text={Locale.SearchChat.Item.View}
  140. />
  141. </div>
  142. </div>
  143. ))}
  144. </div>
  145. </div>
  146. </div>
  147. </ErrorBoundary>
  148. );
  149. }