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 all 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
167 changes: 167 additions & 0 deletions app/components/search-chat.tsx
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);
Comment on lines +26 to +29
Copy link

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 of useRef for tracking changes in the input value can provide a more React idiomatic approach and simplify the code.

- const previousValueRef = useRef<string>("");
+ const [previousValue, setPreviousValue] = useState<string>("");

Update the logic accordingly to use setPreviousValue.

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [searchResults, setSearchResults] = useState<Item[]>([]);
const previousValueRef = useRef<string>("");
const searchInputRef = useRef<HTMLInputElement>(null);
const [searchResults, setSearchResults] = useState<Item[]>([]);
const [previousValue, setPreviousValue] = useState<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(() => {
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);
}, [doSearch]);
Comment on lines +70 to +86
Copy link

Choose a reason for hiding this comment

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

Use a debounced input change handler instead of a timer.

Using a timer to check for input changes is inefficient. Consider using a debounced input change handler with useState and useEffect to manage search execution.

import { useDebounce } from 'use-debounce'; // Assuming a debounce hook is available

// Inside the component
const [debouncedSearchText] = useDebounce(searchInputRef.current?.value, 500);

useEffect(() => {
  if (debouncedSearchText) {
    const result = doSearch(debouncedSearchText);
    setSearchResults(result);
  }
}, [debouncedSearchText, doSearch]);

This approach avoids unnecessary checks and improves performance.


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>
);
}
8 changes: 7 additions & 1 deletion app/constant.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import path from "path";

export const OWNER = "ChatGPTNextWeb";
export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
Expand Down Expand Up @@ -41,6 +43,7 @@ export enum Path {
Sd = "/sd",
SdNew = "/sd-new",
Artifacts = "/artifacts",
SearchChat = "/search-chat",
}

export enum ApiPath {
Expand Down Expand Up @@ -475,4 +478,7 @@ export const internalAllowedWebDavEndpoints = [
];

export const DEFAULT_GA_ID = "G-89WN60ZK2E";
export const PLUGINS = [{ name: "Stable Diffusion", path: Path.Sd }];
export const PLUGINS = [
{ name: "Stable Diffusion", path: Path.Sd },
{ name: "Search Chat", path: Path.SearchChat },
];
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/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,21 @@ const ar: PartialLocaleType = {
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/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,21 @@ const bn: PartialLocaleType = {
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/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/cs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,21 @@ const cs: PartialLocaleType = {
FineTuned: {
Sysmessage: "Jste asistent",
},
SearchChat: {
Name: "Hledat",
Page: {
Title: "Hledat v historii chatu",
Search: "Zadejte hledané klíčové slovo",
NoResult: "Nebyly nalezeny žádné výsledky",
NoData: "Žádná data",
Loading: "Načítání",

SubTitle: (count: number) => `Nalezeno ${count} výsledků`,
},
Item: {
View: "Zobrazit",
},
},
Mask: {
Name: "Maska",
Page: {
Expand Down
15 changes: 15 additions & 0 deletions app/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,21 @@ const de: PartialLocaleType = {
FineTuned: {
Sysmessage: "Du bist ein Assistent",
},
SearchChat: {
Name: "Suche",
Page: {
Title: "Chatverlauf durchsuchen",
Search: "Suchbegriff eingeben",
NoResult: "Keine Ergebnisse gefunden",
NoData: "Keine Daten",
Loading: "Laden",

SubTitle: (count: number) => `${count} Ergebnisse gefunden`,
},
Item: {
View: "Ansehen",
},
},
Mask: {
Name: "Masken",
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/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,21 @@ const es: PartialLocaleType = {
FineTuned: {
Sysmessage: "Eres un asistente",
},
SearchChat: {
Name: "Buscar",
Page: {
Title: "Buscar en el historial de chat",
Search: "Ingrese la palabra clave de búsqueda",
NoResult: "No se encontraron resultados",
NoData: "Sin datos",
Loading: "Cargando",

SubTitle: (count: number) => `Se encontraron ${count} resultados`,
},
Item: {
View: "Ver",
},
},
Mask: {
Name: "Máscara",
Page: {
Expand Down
15 changes: 15 additions & 0 deletions app/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,21 @@ const fr: PartialLocaleType = {
FineTuned: {
Sysmessage: "Vous êtes un assistant",
},
SearchChat: {
Name: "Recherche",
Page: {
Title: "Rechercher dans l'historique des discussions",
Search: "Entrez le mot-clé de recherche",
NoResult: "Aucun résultat trouvé",
NoData: "Aucune donnée",
Loading: "Chargement",

SubTitle: (count: number) => `${count} résultats trouvés`,
},
Item: {
View: "Voir",
},
},
Mask: {
Name: "Masque",
Page: {
Expand Down
Loading
Loading