search-chat.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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 setDefaultItems = () => {
  23. // setSearchResults(
  24. // sessions.slice(1, 7).map((session, index) => {
  25. // return {
  26. // id: index,
  27. // name: session.topic,
  28. // content: session.messages[0].content as string, //.map((m) => m.content).join("\n")
  29. // };
  30. // }),
  31. // );
  32. // };
  33. // useEffect(() => {
  34. // setDefaultItems();
  35. // }, []);
  36. const previousValueRef = useRef<string>("");
  37. const searchInputRef = useRef<HTMLInputElement>(null);
  38. const doSearch = (text: string) => {
  39. const lowerCaseText = text.toLowerCase();
  40. const results: Item[] = [];
  41. sessions.forEach((session, index) => {
  42. const fullTextContents: string[] = [];
  43. session.messages.forEach((message) => {
  44. const content = message.content as string;
  45. const lowerCaseContent = content.toLowerCase();
  46. // 全文搜索
  47. let pos = lowerCaseContent.indexOf(lowerCaseText);
  48. while (pos !== -1) {
  49. const start = Math.max(0, pos - 35);
  50. const end = Math.min(content.length, pos + lowerCaseText.length + 35);
  51. fullTextContents.push(content.substring(start, end));
  52. pos = lowerCaseContent.indexOf(
  53. lowerCaseText,
  54. pos + lowerCaseText.length,
  55. );
  56. }
  57. });
  58. if (fullTextContents.length > 0) {
  59. results.push({
  60. id: index,
  61. name: session.topic,
  62. content: fullTextContents.join("... "), // 使用...连接不同消息中的内容
  63. });
  64. }
  65. });
  66. // 按内容长度排序
  67. results.sort((a, b) => b.content.length - a.content.length);
  68. return results;
  69. };
  70. useEffect(() => {
  71. const intervalId = setInterval(() => {
  72. if (searchInputRef.current) {
  73. const currentValue = searchInputRef.current.value;
  74. if (currentValue !== previousValueRef.current) {
  75. if (currentValue.length > 0) {
  76. const result = doSearch(currentValue);
  77. setSearchResults(result);
  78. }
  79. previousValueRef.current = currentValue;
  80. }
  81. }
  82. }, 1000);
  83. // Cleanup the interval on component unmount
  84. return () => clearInterval(intervalId);
  85. }, []);
  86. return (
  87. <ErrorBoundary>
  88. <div className={styles["mask-page"]}>
  89. {/* header */}
  90. <div className="window-header">
  91. <div className="window-header-title">
  92. <div className="window-header-main-title">
  93. {Locale.SearchChat.Page.Title}
  94. </div>
  95. <div className="window-header-submai-title">
  96. {Locale.SearchChat.Page.SubTitle(searchResults.length)}
  97. </div>
  98. </div>
  99. <div className="window-actions">
  100. <div className="window-action-button">
  101. <IconButton
  102. icon={<CloseIcon />}
  103. bordered
  104. onClick={() => navigate(-1)}
  105. />
  106. </div>
  107. </div>
  108. </div>
  109. <div className={styles["mask-page-body"]}>
  110. <div className={styles["mask-filter"]}>
  111. {/**搜索输入框 */}
  112. <input
  113. type="text"
  114. className={styles["search-bar"]}
  115. placeholder={Locale.SearchChat.Page.Search}
  116. autoFocus
  117. ref={searchInputRef}
  118. onKeyDown={(e) => {
  119. if (e.key === "Enter") {
  120. e.preventDefault();
  121. const searchText = e.currentTarget.value;
  122. if (searchText.length > 0) {
  123. const result = doSearch(searchText);
  124. setSearchResults(result);
  125. }
  126. }
  127. }}
  128. />
  129. </div>
  130. <div>
  131. {searchResults.map((item) => (
  132. <div className={styles["mask-item"]} key={item.id}>
  133. {/** 搜索匹配的文本 */}
  134. <div className={styles["mask-header"]}>
  135. <div className={styles["mask-title"]}>
  136. <div className={styles["mask-name"]}>{item.name}</div>
  137. {item.content.slice(0, 70)}
  138. </div>
  139. </div>
  140. {/** 操作按钮 */}
  141. <div className={styles["mask-actions"]}>
  142. <IconButton
  143. icon={<EyeIcon />}
  144. text={Locale.SearchChat.Item.View}
  145. onClick={() => {
  146. navigate(Path.Chat);
  147. selectSession(item.id);
  148. }}
  149. />
  150. </div>
  151. </div>
  152. ))}
  153. </div>
  154. </div>
  155. </div>
  156. </ErrorBoundary>
  157. );
  158. }