-
Notifications
You must be signed in to change notification settings - Fork 60.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add a page to search chat history #5274
Changes from 7 commits
cd92036
3da5284
00990dc
98093a1
65ed6b0
8622057
e3f499b
b84bb72
82298a7
fd1c656
7ce2e8f
fcd55df
39d7d9f
b529118
64a0ffe
6649fbd
09a9066
e275abd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import { useState, useEffect, useRef } 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 = (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; | ||
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(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 旁观,使用定时器来每秒判断值变化会不会不太好?使用useState绑定input的value,添加到deps中,并做防抖?doSearch函数也可以用useCallBack包裹一下? 仅旁观建议,如有说错请忽略🫣 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 好建议!我有空更新一下 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
1. 防抖方案新建分支尝试实现了一下你提到的方案,https://github.com/Movelocity/ChatGPT-Next-Web/tree/test/search-history 使用防抖(debounce)机制时,防抖函数会在每次输入时重置计时器,只有在输入停止并且超过防抖时间后,才会执行搜索操作。 举个例子,当用户想搜索 相比之下,使用定时器方案可以频繁地检查输入框的值,并立即触发搜索操作。这样可以显著提高搜索功能的响应速度,用户在快速输入时,不必等到整个单词输入完成再等待一次防抖的延时。 所以我还是打算保留原方案。 2. useCallbackdoSearch 函数被 useEffect 依赖了,useCallback 确实需要加上 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 算了,每个人都有自己的想法 |
||
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); | ||
}, []); | ||
|
||
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> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using
useState
for tracking input changes.Using
useState
instead ofuseRef
for tracking changes in the input value can provide a more React idiomatic approach and simplify the code.Update the logic accordingly to use
setPreviousValue
.Committable suggestion