forked from ChatGPTNextWeb/ChatGPT-Next-Web
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request ChatGPTNextWeb#5274 from Movelocity/feat/search-hi…
…story feat: add a page to search chat history
- Loading branch information
Showing
23 changed files
with
471 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
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; | ||
}, []); | ||
|
||
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.