Spaces:
Sleeping
Sleeping
import { useState, useEffect, useRef, useCallback } from "react"; | |
import { ErrorBoundary } from "./error"; | |
import styles from "./mask.module.scss"; | |
import { useNavigate } from "react-router-dom"; | |
import { IconButton } from "./button"; | |
import CloseIcon from "../icons/close.svg"; | |
import EyeIcon from "../icons/eye.svg"; | |
import Locale from "../locales"; | |
import { Path } from "../constant"; | |
import { useChatStore } from "../store"; | |
type Item = { | |
id: number; | |
name: string; | |
content: string; | |
}; | |
export function SearchChatPage() { | |
const navigate = useNavigate(); | |
const chatStore = useChatStore(); | |
const sessions = chatStore.sessions; | |
const selectSession = chatStore.selectSession; | |
const [searchResults, setSearchResults] = useState<Item[]>([]); | |
const previousValueRef = useRef<string>(""); | |
const searchInputRef = useRef<HTMLInputElement>(null); | |
const doSearch = useCallback((text: string) => { | |
const lowerCaseText = text.toLowerCase(); | |
const results: Item[] = []; | |
sessions.forEach((session, index) => { | |
const fullTextContents: string[] = []; | |
session.messages.forEach((message) => { | |
const content = message.content as string; | |
if (!content.toLowerCase || content === "") return; | |
const lowerCaseContent = content.toLowerCase(); | |
// full text search | |
let pos = lowerCaseContent.indexOf(lowerCaseText); | |
while (pos !== -1) { | |
const start = Math.max(0, pos - 35); | |
const end = Math.min(content.length, pos + lowerCaseText.length + 35); | |
fullTextContents.push(content.substring(start, end)); | |
pos = lowerCaseContent.indexOf( | |
lowerCaseText, | |
pos + lowerCaseText.length, | |
); | |
} | |
}); | |
if (fullTextContents.length > 0) { | |
results.push({ | |
id: index, | |
name: session.topic, | |
content: fullTextContents.join("... "), // concat content with... | |
}); | |
} | |
}); | |
// sort by length of matching content | |
results.sort((a, b) => b.content.length - a.content.length); | |
return results; | |
}, [sessions]); | |
useEffect(() => { | |
const intervalId = setInterval(() => { | |
if (searchInputRef.current) { | |
const currentValue = searchInputRef.current.value; | |
if (currentValue !== previousValueRef.current) { | |
if (currentValue.length > 0) { | |
const result = doSearch(currentValue); | |
setSearchResults(result); | |
} | |
previousValueRef.current = currentValue; | |
} | |
} | |
}, 1000); | |
// Cleanup the interval on component unmount | |
return () => clearInterval(intervalId); | |
}, [doSearch]); | |
return ( | |
<ErrorBoundary> | |
<div className={styles["mask-page"]}> | |
{/* header */} | |
<div className="window-header"> | |
<div className="window-header-title"> | |
<div className="window-header-main-title"> | |
{Locale.SearchChat.Page.Title} | |
</div> | |
<div className="window-header-submai-title"> | |
{Locale.SearchChat.Page.SubTitle(searchResults.length)} | |
</div> | |
</div> | |
<div className="window-actions"> | |
<div className="window-action-button"> | |
<IconButton | |
icon={<CloseIcon />} | |
bordered | |
onClick={() => navigate(-1)} | |
/> | |
</div> | |
</div> | |
</div> | |
<div className={styles["mask-page-body"]}> | |
<div className={styles["mask-filter"]}> | |
{/**搜索输入框 */} | |
<input | |
type="text" | |
className={styles["search-bar"]} | |
placeholder={Locale.SearchChat.Page.Search} | |
autoFocus | |
ref={searchInputRef} | |
onKeyDown={(e) => { | |
if (e.key === "Enter") { | |
e.preventDefault(); | |
const searchText = e.currentTarget.value; | |
if (searchText.length > 0) { | |
const result = doSearch(searchText); | |
setSearchResults(result); | |
} | |
} | |
}} | |
/> | |
</div> | |
<div> | |
{searchResults.map((item) => ( | |
<div | |
className={styles["mask-item"]} | |
key={item.id} | |
onClick={() => { | |
navigate(Path.Chat); | |
selectSession(item.id); | |
}} | |
style={{ cursor: "pointer" }} | |
> | |
{/** 搜索匹配的文本 */} | |
<div className={styles["mask-header"]}> | |
<div className={styles["mask-title"]}> | |
<div className={styles["mask-name"]}>{item.name}</div> | |
{item.content.slice(0, 70)} | |
</div> | |
</div> | |
{/** 操作按钮 */} | |
<div className={styles["mask-actions"]}> | |
<IconButton | |
icon={<EyeIcon />} | |
text={Locale.SearchChat.Item.View} | |
/> | |
</div> | |
</div> | |
))} | |
</div> | |
</div> | |
</div> | |
</ErrorBoundary> | |
); | |
} | |