Skip to content
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

Merged
merged 18 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/components/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
loading: () => <Loading noLogo />,
});

const SearchChat = dynamic(
async () => (await import("./search-chat")).SearchChatPage,
{
loading: () => <Loading noLogo />,
},
);

const Sd = dynamic(async () => (await import("./sd")).Sd, {
loading: () => <Loading noLogo />,
});
Expand Down Expand Up @@ -174,6 +181,7 @@ function Screen() {
<Route path={Path.Home} element={<Chat />} />
<Route path={Path.NewChat} element={<NewChat />} />
<Route path={Path.Masks} element={<MaskPage />} />
<Route path={Path.SearchChat} element={<SearchChat />} />
<Route path={Path.Chat} element={<Chat />} />
<Route path={Path.Settings} element={<Settings />} />
</Routes>
Expand Down
177 changes: 177 additions & 0 deletions app/components/search-chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
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 setDefaultItems = () => {
// setSearchResults(
// sessions.slice(1, 7).map((session, index) => {
// return {
// id: index,
// name: session.topic,
// content: session.messages[0].content as string, //.map((m) => m.content).join("\n")
// };
// }),
// );
// };
// useEffect(() => {
// setDefaultItems();
// }, []);

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();

// 全文搜索
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("... "), // 使用...连接不同消息中的内容
});
}
});

// 按内容长度排序
results.sort((a, b) => b.content.length - a.content.length);

return results;
};

useEffect(() => {
const intervalId = setInterval(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

旁观,使用定时器来每秒判断值变化会不会不太好?使用useState绑定input的value,添加到deps中,并做防抖?doSearch函数也可以用useCallBack包裹一下? 仅旁观建议,如有说错请忽略🫣

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好建议!我有空更新一下

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

旁观,使用定时器来每秒判断值变化会不会不太好?使用useState绑定input的value,添加到deps中,并做防抖?doSearch函数也可以用useCallBack包裹一下? 仅旁观建议,如有说错请忽略🫣

1. 防抖方案

新建分支尝试实现了一下你提到的方案,https://github.com/Movelocity/ChatGPT-Next-Web/tree/test/search-history

使用防抖(debounce)机制时,防抖函数会在每次输入时重置计时器,只有在输入停止并且超过防抖时间后,才会执行搜索操作。
我把防抖时间设为1000毫秒。现在每隔500毫秒输入一个字符,最终只会在输入完成并且没有进一步输入的情况下(即等待时间超过防抖时间)触发一次搜索操作。

举个例子,当用户想搜索 javascript,输入 javas 后就大概率已经能搜索到完整的列表了。而每隔500毫秒输入 j a v a s c r i p t 的情况下,最终只会在输入完成(javascript 整个单词)并且等待超过1000毫秒后,才会执行一次以 javascript 为关键词的搜索

相比之下,使用定时器方案可以频繁地检查输入框的值,并立即触发搜索操作。这样可以显著提高搜索功能的响应速度,用户在快速输入时,不必等到整个单词输入完成再等待一次防抖的延时。

所以我还是打算保留原方案。

2. useCallback

doSearch 函数被 useEffect 依赖了,useCallback 确实需要加上

Copy link
Contributor

Choose a reason for hiding this comment

The 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}>
{/** 搜索匹配的文本 */}
<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}
onClick={() => {
navigate(Path.Chat);
selectSession(item.id);
}}
/>
</div>
</div>
))}
</div>
</div>
</div>
</ErrorBoundary>
);
}
11 changes: 10 additions & 1 deletion app/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import DeleteIcon from "../icons/delete.svg";
import MaskIcon from "../icons/mask.svg";
import DragIcon from "../icons/drag.svg";
import DiscoveryIcon from "../icons/discovery.svg";

import SearchIcon from "../icons/zoom.svg";
import Locale from "../locales";

import { useAppConfig, useChatStore } from "../store";
Expand Down Expand Up @@ -250,6 +250,15 @@ export function SideBar(props: { className?: string }) {
onClick={() => setShowPluginSelector(true)}
shadow
/>
<IconButton
icon={<SearchIcon />}
text={shouldNarrow ? undefined : Locale.SearchChat.Name}
className={styles["sidebar-bar-button"]}
onClick={() =>
navigate(Path.SearchChat, { state: { fromHome: true } })
}
shadow
/>
</div>
{showPluginSelector && (
<Selector
Expand Down
1 change: 1 addition & 0 deletions app/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export enum Path {
Sd = "/sd",
SdNew = "/sd-new",
Artifacts = "/artifacts",
SearchChat = "/search-chat",
}

export enum ApiPath {
Expand Down
1 change: 1 addition & 0 deletions app/icons/zoom.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions app/locales/cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,21 @@ const cn = {
FineTuned: {
Sysmessage: "你是一个助手",
},
SearchChat: {
Name: "搜索",
Page: {
Title: "搜索聊天记录",
Search: "输入搜索关键词",
NoResult: "没有找到结果",
NoData: "没有数据",
Loading: "加载中",

SubTitle: (count: number) => `搜索到 ${count} 条结果`,
},
Item: {
View: "查看",
},
},
Mask: {
Name: "面具",
Page: {
Expand Down
15 changes: 15 additions & 0 deletions app/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,21 @@ const en: LocaleType = {
FineTuned: {
Sysmessage: "You are an assistant that",
},
SearchChat: {
Name: "Search",
Page: {
Title: "Search Chat History",
Search: "Enter search query to search chat history",
NoResult: "No results found",
NoData: "No data",
Loading: "Loading...",

SubTitle: (count: number) => `Found ${count} results`,
},
Item: {
View: "View",
},
},
Mask: {
Name: "Mask",
Page: {
Expand Down
15 changes: 15 additions & 0 deletions app/locales/jp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,21 @@ const jp: PartialLocaleType = {
},
Plugin: { Name: "プラグイン" },
FineTuned: { Sysmessage: "あなたはアシスタントです" },
SearchChat: {
Name: "検索",
Page: {
Title: "チャット履歴を検索",
Search: "キーワードを入力してください",
NoResult: "結果が見つかりませんでした",
NoData: "データがありません",
Loading: "読み込み中...",

SubTitle: (count: number) => `${count} 件の結果を見つけました`,
},
Item: {
View: "表示",
},
},
Mask: {
Name: "キャラクタープリセット",
Page: {
Expand Down
15 changes: 15 additions & 0 deletions app/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,21 @@ const ru: PartialLocaleType = {
FineTuned: {
Sysmessage: "Вы - ассистент, который",
},
SearchChat: {
Name: "Поиск",
Page: {
Title: "Поиск в истории чата",
Search: "Введите текст для поиска",
NoResult: "Результаты не найдены",
NoData: "Данные отсутствуют",
Loading: "Загрузка...",

SubTitle: (count: number) => `Найдено результатов: ${count}`,
},
Item: {
View: "Просмотр",
},
},
Mask: {
Name: "Маска",
Page: {
Expand Down
Loading
Loading